diff --git a/.gitignore b/.gitignore index 62e84356..cc3fb06a 100644 --- a/.gitignore +++ b/.gitignore @@ -157,3 +157,8 @@ gradle-app.setting # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # gradle/wrapper/gradle-wrapper.properties + +### ROOSTER-specific + +# motion profiles +*.csv diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 806c7b0e..676896fb 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -9,9 +9,9 @@ - - - + + + diff --git a/.idea/runConfigurations/Deploy_AccelerationCharacterizationRobot.xml b/.idea/runConfigurations/Deploy_AccelerationCharacterizationRobot.xml new file mode 100644 index 00000000..493ed106 --- /dev/null +++ b/.idea/runConfigurations/Deploy_AccelerationCharacterizationRobot.xml @@ -0,0 +1,21 @@ + + + + + + + true + + + diff --git a/.idea/runConfigurations/Deploy_HeadingPIDPipelineTestRobot.xml b/.idea/runConfigurations/Deploy_HeadingPIDPipelineTestRobot.xml new file mode 100644 index 00000000..e75f62f7 --- /dev/null +++ b/.idea/runConfigurations/Deploy_HeadingPIDPipelineTestRobot.xml @@ -0,0 +1,21 @@ + + + + + + + true + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Deploy_MotionProfilePipelineTestRobot.xml b/.idea/runConfigurations/Deploy_MotionProfilePipelineTestRobot.xml new file mode 100644 index 00000000..716ec911 --- /dev/null +++ b/.idea/runConfigurations/Deploy_MotionProfilePipelineTestRobot.xml @@ -0,0 +1,21 @@ + + + + + + + true + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Deploy_ProfileContainerTestingRobot.xml b/.idea/runConfigurations/Deploy_ProfileContainerTestingRobot.xml new file mode 100644 index 00000000..d2ee725f --- /dev/null +++ b/.idea/runConfigurations/Deploy_ProfileContainerTestingRobot.xml @@ -0,0 +1,21 @@ + + + + + + + true + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Deploy_TurningRatePIDPipelineTestRobot.xml b/.idea/runConfigurations/Deploy_TurningRatePIDPipelineTestRobot.xml new file mode 100644 index 00000000..38da5f8f --- /dev/null +++ b/.idea/runConfigurations/Deploy_TurningRatePIDPipelineTestRobot.xml @@ -0,0 +1,21 @@ + + + + + + + true + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Deploy_VelocityCharacterizationRobot.xml b/.idea/runConfigurations/Deploy_VelocityCharacterizationRobot.xml new file mode 100644 index 00000000..b8a25465 --- /dev/null +++ b/.idea/runConfigurations/Deploy_VelocityCharacterizationRobot.xml @@ -0,0 +1,21 @@ + + + + + + + true + + + diff --git a/.idea/runConfigurations/Deploy_WheelbaseTestRobot.xml b/.idea/runConfigurations/Deploy_WheelbaseTestRobot.xml new file mode 100644 index 00000000..72f542de --- /dev/null +++ b/.idea/runConfigurations/Deploy_WheelbaseTestRobot.xml @@ -0,0 +1,21 @@ + + + + + + + true + + + diff --git a/README.md b/README.md index 4b61d2c0..1a11a187 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,7 @@ Advanced closed-loop drive code for a tank drive. `org.team1540.rooster.drive.pipeline` -A flexible system for controlling a robot drive. More docs [here](docs/Drive%20Pipelines.md). - -#### Motion Profiling - -`org.team1540.rooster.motionprofiling` - -A system for executing motion profiles on a tank drive. +A flexible system for controlling a robot drive. More docs [here](docs/Drive%20Pipelines.md), with a specific section on motion profiling [here](docs/Motion%20Profiling.md). #### Power Management diff --git a/docs/Drive Pipelines.md b/docs/Drive Pipelines.md index df7d6068..cbd2443f 100644 --- a/docs/Drive Pipelines.md +++ b/docs/Drive Pipelines.md @@ -30,7 +30,7 @@ Executable pipeline = new SimpleJoystickInput(new Joystick(0), 1, 5, false, fals Breakdown: - ` SimpleJoystickInput(new Joystick(0), 1, 5, false, false)`: Takes values from a joystick on port 0, with axis 1 as the left and axis 5 on the right, inverting neither -- `.then(new TalonSRXOutput(leftTalon, rightTalon))`: Sends the output of the previous `SimpleJoystickInput` to your `leftTalon` and `rightTalon`. Since the output of `SimpleJoystickInput` only sets the feed-forward (i.e. raw throttle) term, it'll automatically use `PercentOutput` output mode. +- `.then(new CTREOutput(leftTalon, rightTalon))`: Sends the output of the previous `SimpleJoystickInput` to your `leftTalon` and `rightTalon`. Since the output of `SimpleJoystickInput` only sets the feed-forward (i.e. raw throttle) term, it'll automatically use `PercentOutput` output mode. ### Execute a Motion Profile @@ -44,7 +44,7 @@ Breakdown: - `ProfileInput` takes values from two provided `MotionProfile` instances and returns the setpoint for the current time. The timer starts when the pipeline is first executed. - `OpenLoopFeedForward` takes the velocity and acceleration setpoints from the `ProfileInput` and calculates a suitable feed-forward for them using coefficients you provide, Oblarg-style. It then passes those velocities down. -- `TalonSRXOutput`, since it's receiving position setpoints from the `ProfileInput`, tells the Talon closed-loop to PID to those setpoints while providing the feed-forward from the `OpenLoopFeedForward` as an additional bump term. +- `CTREOutput`, since it's receiving position setpoints from the `ProfileInput`, tells the Talon closed-loop to PID to those setpoints while providing the feed-forward from the `OpenLoopFeedForward` as an additional bump term. ### Use in a Command @@ -64,14 +64,8 @@ Most "stock" pipeline elements pass around `TankDriveData` instances to encapsul An input that returns the same `TankDriveData` every time: ```java -TankDriveData tankDriveData = new TankDriveData( - new DriveData(0), - new DriveData(0), - OptionalDouble.empty(), - OptionalDouble.empty()); - -Executable pipeline = ((Input) () -> tankDriveData) - .then(new CTREOutput(leftTalon, rightTalon)) +Executable pipeline = ((Input) () -> new TankDriveData().withVelocities(0, 0)) + .then(new CTREOutput(leftTalon, rightTalon)) ``` #### Custom Processor @@ -80,21 +74,9 @@ A processor that multiplies the received position by two: ```java Executable pipeline = new SimpleJoystickInput(new Joystick(0), 1, 5, false, false) - .then(data -> return new TankDriveData( - new DriveData( - d.left.position.isPresent() ? OptionalDouble.of(d.left.position.getAsDouble() * 2) : d.left.position, - d.left.velocity, - d.left.acceleration, - d.left.additionalFeedForward - ), - new DriveData( - d.right.position.isPresent() ? OptionalDouble.of(d.right.position.getAsDouble() * 2) : d.right.position, - d.right.velocity, - d.right.acceleration, - d.right.additionalFeedForward - ), - d.heading, d.turningRate); - )) + .then(data -> data.modifyPositions( + (left) -> left.isPresent() ? left.getAsDouble() * 2 : left, + (right) -> right.isPresent() ? right.getAsDouble() * 2 : right) .then(new CTREOutput(leftTalon, rightTalon)) ``` diff --git a/docs/Motion Profiling.md b/docs/Motion Profiling.md new file mode 100644 index 00000000..fa51743a --- /dev/null +++ b/docs/Motion Profiling.md @@ -0,0 +1,153 @@ +# Motion Profiling with ROOSTER + +This is a start-to-finish guide to implementing the robot side of motion profiling using ROOSTER. Generation is a separate matter (and not covered by ROOSTER at the moment)—this covers the steps after you've created motion profile CSV files and deployed them to a folder on the RoboRIO. + +## Loading Profiles + +Profiles can be pre-loaded from a designated folder on the RoboRIO using a `ProfileContainer`. A `ProfileContainer` automatically loads motion profiles from Pathfinder-style CSV files and keeps them in RAM until they are needed. + +For an example, let's say that your folder structure is like so: + +``` +/home/lvuser/ + profiles/ + foo_left.csv + foo_right.csv + bar_left.csv + bar_right.csv +``` + +To load these into two motion profiles named "foo" and "bar" (each containing left and right sides), use a `ProfileContainer` as follows: + +```java +import edu.wpi.first.wpilibj.IterativeRobot; +import org.team1540.rooster.motionprofiling.ProfileContainer; + +public class Robot extends IterativeRobot { + public static final ProfileContainer profiles; + + @Override + public void robotInit() { + profiles = new ProfileContainer(new File("/home/lvuser/profiles")); + } +} +``` + +You can then access the left and right profiles in a `Command` or anywhere else you might need them: + +```java +import edu.wpi.first.wpilibj.command.Command; +import org.team1540.rooster.motionprofiling.ProfileContainer; +import org.team1540.rooster.motionprofiling.MotionProfile; + +public class ProfileCommand extends Command { + @Override + protected void initialize() { + ProfileContainer.DriveProfile profiles = Robot.profiles.get("foo"); + MotionProfile left = profiles.getLeft(); + MotionProfile right = profiles.getRight(); + + // do something with the profiles + } + + // isFinished, etc. +} +``` + +### Changing the Suffix + +Let's say your profiles use a different naming scheme. For instance: + +``` +/home/lvuser/ + profiles/ + foo.left.csv + foo.right.csv + bar.left.csv + bar.right.csv +``` + +The `ProfileContainer` supports differing file formats by specifying the suffixes in the constructor: + +```java +import edu.wpi.first.wpilibj.IterativeRobot; +import org.team1540.rooster.motionprofiling.ProfileContainer; + +public class Robot extends IterativeRobot { + public static final ProfileContainer profiles; + + @Override + public void robotInit() { + profiles = new ProfileContainer(new File("/home/lvuser/profiles"), ".left.csv", ".right.csv"); + } +} +``` + +Once you have your left and right side profiles, you need to do something with them. Execution is a fairly complicated topic involving a number of different steps. + +## Empirical Testing + +The first step in accurate profile execution is an accurate model of your robot's behavior. While all of these quantities can be calculated theoretically, it is much better to do them empirically. + +### Ticks Per Unit and Track Width Testing + +Ticks per unit (TPU) is a number representing the correspondence between encoder ticks and real-world position of the wheels on the ground. While TPU can be theoretically calculated by simply dividing the wheel circumference by the number of ticks per rotation of the wheel, it is better measured empirically to account for tread wear and other factors. + +Track width (also erroneously called "wheelbase" in certain libraries) is the distance between your robot's left and right wheels, and its theoretical calculation is even simpler—just measure the distance. However, wheelbase is very susceptible to effects from wheel-scrub and other factors (especially if you are using multiple sets of traction wheels). Note that this measurement is often fed into your generation solution, so you may want to go back and remake your paths. + +Both track width and TPU can be tested with the `WheelbaseTestRobot`, in the `org.team1540.rooster.util.robots` package. `WheelbaseTestRobot` is a fully self-contained robot class that should be deployed onto your robot. To deploy the `WheelbaseTestRobot` from a project with ROOSTER already added as a dependency, set your robot class to `org.team1540.rooster.util.robots.WheelbaseTestRobot` and deploy. + +Once you deploy and connect a driver station, it will throw several values onto SmartDashboard or Shuffleboard. Set your left and right motor IDs and inversions as necessary, then run the Reset and Zero commands. + +#### Ticks Per Unit Testing + +To measure TPU, push your robot some defined distance (for example, 10 feet) and note the LPOS and RPOS readouts on the SmartDashboard. (They should be near identical.) Then divide them by the distance your robot traveled to get your ticks per unit. For example, if you push your robot ten feet and measure LPOS and RPOS of about 4500, then your ticks per unit (in this case ticks per foot) would be 450. + +#### Track Width Testing + +Run the Zero command again, then enter your TPU value from the last step into the "encoderTPU" readout. Additionally, it is reccomended to set the "brake" readout to `true` and run the Reset command. Then, enable the robot (in tele-op mode) and hold down the A button on controller 0 until the robot completes ten full revolutions. Your empirically calculated wheelbase width (in whatever units you used to calculate your TPU) will then be placed in the "Calculated Width" readout. + +### Feed-Forward Testing + +The next step in your testing is creating an accurate model of your drivetrain performance. A standard (non-shifting) FRC drivetrain's performance can be generally described with three numbers: namely, a velocity constant, a voltage intercept, and an acceleration constant. (For shifting drivetrains, you simply have two sets: one for each gear.) The velocity constant is essentially how many additional volts must be provided to the motor to achieve a speed increase of 1 unit. The acceleration constant is the same, but with acceleration. The voltage intercept is a bit more interesting: it is the y-intercept of a line with velocity on the x-axis and voltage on the y-axis. In other words, it is the voltage needed to overcome static friction effects. + +![response curve](img/responsecurve.png) + +This picture shows the approximate velocity-voltage curve (with voltage on the y-axis) for our 2018 robot. Note how the Y-intercept is nonzero; i.e. a velocity of just above zero requires a good deal of additional voltage to overcome static friction. Testing shows that in FRC drivetrains, this static friction remains regardless of additional velocity, as seen on the graph. + +#### Determining Velocity Curve + +Determining your velocity constant and voltage intercept is done in the same test. The theory of the test is essentially collecting data on the motors' velocity as the voltage is slowly increased, and then using linear regression to fit a line of best fit to the data. This process is handled by the `VelocityCharacterizationRobot` in ROOSTER. Deploy the `VelocityCharacterizationRobot` by following the steps described [here](#Ticks Per Unit and Track Width Testing) but using `org.team1540.rooster.util.robots.VelocityCharacterizationRobot`. Configure your inversions, motor IDs, and TPU as described above, then run the Reset command. Place the robot so that it has as long a distance as possible to drive straight, enable the robot, and hold down the A button until your robot reaches an obstacle. Your left and right kV (velocity constant) and vIntercept (voltage intercept) should be placed on the SmartDashboard. Note that these constants will use volts as their unit, and it is likely that you will want the feed-forwards to be in percent throttle—in that case, simply divide them by 12. + +#### Determining Acceleration Constant + +In general, it is theoretically possible to determine the acceleration constant empirically. However, if you find yourself unable to do so, you can divide 12 by your robot's theoretical maximum acceleration. The maximum acceleration is equal to your number of motors, multiplied by each motor's stall torque, multiplied by your gear ratio, divided by your wheel radius, divided by the mass of your robot. (Make sure your units line up when making this calculation: maximum torque is often given in newton-meters for FRC motors.) + +Documentation on how to determine acceleration coefficient empirically coming once we figure out how to do it properly. + +## Running the Profile + +Motion profile execution is tightly integrated into the drive pipeline system. As such, creating a motion profile execution system is as simple as constructing a drive pipeline with a number of stages, and wrapping it in a `SimpleAsyncCommand`: + +```java +Executable pipeline = new ProfileInput(leftProfile, rightProfile) + .andThen(new FeedForwardProcessor(velocityConstant, velocityIntercept, accelerationConstant)) + .andThen(new HeadingPIDProcessor(hdgP, hdgI, hdgD, Gyro::getHeadingInRadians, true)) + .andThen(new UnitScaler(tpu, 0.1)) + .andThen(CTREOutput(leftMotor1, rightMotor1)); + +Command command = new SimpleAsyncCommand("Run profile", 20, pipeline); +command.start(); +``` + +Breaking the pipeline segments down in detail: + +- The `ProfileInput` takes left and right profiles and runs through them according to a timer. +- The `FeedForwardProcessor` processes velocity, acceleration, and voltage constants and adds them to the additional feed-forward. +- The `HeadingPIDProcessor` adds a heading-based PID loop to keep the robot driving at the correct heading, and adds the output to the position setpoint on each side. Note that it takes a lambda or method reference in addition to coefficients. This is used for getting the heading in radians from the gyro. (Note that the range should be from -π to π as described in its documentation.) +- The `UnitScaler` re-scales units from whatever your profile units happen to be in to the raw encoder ticks accepted by the motors. +- The `CTREOutput` takes the fully processed command and sends it to the left and right motors. + +Since drive pipelines are `Executable`, they can be passed to a `SimpleCommand`. `SimpleAsyncCommand` is a variant of `SimpleCommand` which executes the code in a different thread at a fixed interval—in this case, 20 milliseconds. + +The `HeadingPIDProcessor` should be tuned in the manner of a normal PID loop; additionally, you should tune the Talon SRX closed-loop position PID. (More on how to do this coming soon.) \ No newline at end of file diff --git a/docs/img/responsecurve.png b/docs/img/responsecurve.png new file mode 100644 index 00000000..97262eb5 Binary files /dev/null and b/docs/img/responsecurve.png differ diff --git a/src/main/java/org/team1540/rooster/drive/JoystickScaling.java b/src/main/java/org/team1540/rooster/drive/JoystickScaling.java index 963064d8..90ed813a 100644 --- a/src/main/java/org/team1540/rooster/drive/JoystickScaling.java +++ b/src/main/java/org/team1540/rooster/drive/JoystickScaling.java @@ -1,6 +1,6 @@ package org.team1540.rooster.drive; -import org.team1540.rooster.drive.pipeline.Processor; +import org.team1540.rooster.functional.Processor; @FunctionalInterface public interface JoystickScaling extends Processor { diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/AdvancedArcadeJoystickInput.java b/src/main/java/org/team1540/rooster/drive/pipeline/AdvancedArcadeJoystickInput.java index 2db11286..5ae80848 100644 --- a/src/main/java/org/team1540/rooster/drive/pipeline/AdvancedArcadeJoystickInput.java +++ b/src/main/java/org/team1540/rooster/drive/pipeline/AdvancedArcadeJoystickInput.java @@ -4,6 +4,7 @@ import java.util.function.DoubleSupplier; import org.jetbrains.annotations.NotNull; import org.team1540.rooster.Utilities; +import org.team1540.rooster.functional.Input; /** * Modified arcade drive joystick input. @@ -15,17 +16,14 @@ * here. *

* This class is an {@link Input} that provides a {@link TankDriveData}. The resulting {@link - * TankDriveData} will have the left and right velocities set with units corresponding to the max - * velocity set on construction, as well as the turning rate in radians per second. All other values - * are empty {@link OptionalDouble OptionalDoubles}. + * TankDriveData} will have the left and right feed-forwards set to throttles between -1 and 1. All + * other values are empty {@link OptionalDouble OptionalDoubles}. * * @see Input * @see FeedForwardProcessor */ public class AdvancedArcadeJoystickInput implements Input { - private double maxVelocity; - private double trackWidth; private boolean reverseBackwards; @NotNull private DoubleSupplier throttleInput; @@ -37,32 +35,22 @@ public class AdvancedArcadeJoystickInput implements Input { /** * Creates a new {@code AdvancedArcadeJoystickInput} that does not reverse while going backwards. * - * @param maxVelocity The maximum velocity of the robot; joystick values will be scaled to this - * amount. This should be in position units per second to keep with the specification of {@link - * TankDriveData}. - * @param trackWidth The track width of the robot (distance between the wheels); this should be in - * the same position units as maxVelocity. * @param throttleInput A {@link DoubleSupplier} that supplies the input for the throttle, from -1 * to 1 inclusive. * @param softTurnInput A {@link DoubleSupplier} that supplies the input for the soft-turn, from * -1 (full left) to 1 (full right) inclusive. * @param hardTurnInput A {@link DoubleSupplier} that supplies the input for the soft-turn, from + * -1 to 1 inclusive. */ - public AdvancedArcadeJoystickInput(double maxVelocity, double trackWidth, - @NotNull DoubleSupplier throttleInput, + public AdvancedArcadeJoystickInput(@NotNull DoubleSupplier throttleInput, @NotNull DoubleSupplier softTurnInput, @NotNull DoubleSupplier hardTurnInput) { - this(maxVelocity, trackWidth, true, throttleInput, softTurnInput, hardTurnInput); + this(false, throttleInput, softTurnInput, hardTurnInput); } /** * Creates a new {@code AdvancedArcadeJoystickInput}. * - * @param maxVelocity The maximum velocity of the robot; joystick values will be scaled to this - * amount. This should be in position units per second to keep with the specification of {@link - * TankDriveData}. - * @param trackWidth The track width of the robot (distance between the wheels); this should be in - * the same position units as maxVelocity. * @param reverseBackwards If {@code true}, reverses the direction of the soft turn when the * throttle is negative. * @param throttleInput A {@link DoubleSupplier} that supplies the input for the throttle, from -1 @@ -70,13 +58,12 @@ public AdvancedArcadeJoystickInput(double maxVelocity, double trackWidth, * @param softTurnInput A {@link DoubleSupplier} that supplies the input for the soft-turn, from * -1 (full left) to 1 (full right) inclusive. * @param hardTurnInput A {@link DoubleSupplier} that supplies the input for the soft-turn, from + * -1 to 1 inclusive. */ - public AdvancedArcadeJoystickInput(double maxVelocity, double trackWidth, - boolean reverseBackwards, @NotNull DoubleSupplier throttleInput, + public AdvancedArcadeJoystickInput(boolean reverseBackwards, + @NotNull DoubleSupplier throttleInput, @NotNull DoubleSupplier softTurnInput, @NotNull DoubleSupplier hardTurnInput) { - this.maxVelocity = maxVelocity; - this.trackWidth = trackWidth; this.reverseBackwards = reverseBackwards; this.throttleInput = throttleInput; this.softTurnInput = softTurnInput; @@ -116,18 +103,6 @@ public TankDriveData get() { rightPower = rightPowerRaw; } - // omega (dtheta / dt or yaw rate) is just the difference in velocities (powers) divided by the - // track width - - double leftVelocity = leftPower * maxVelocity; - double rightVelocity = rightPower * maxVelocity; - - double omega = (rightVelocity - leftVelocity) / trackWidth; - - return new TankDriveData( - new DriveData(OptionalDouble.of(leftVelocity)), - new DriveData(OptionalDouble.of(rightVelocity)), - OptionalDouble.empty(), - OptionalDouble.of(omega)); + return new TankDriveData().withAdditionalFeedForwards(leftPower, rightPower); } } diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/CTREOutput.java b/src/main/java/org/team1540/rooster/drive/pipeline/CTREOutput.java index 49825fcd..1ceb9eb2 100644 --- a/src/main/java/org/team1540/rooster/drive/pipeline/CTREOutput.java +++ b/src/main/java/org/team1540/rooster/drive/pipeline/CTREOutput.java @@ -6,6 +6,7 @@ import java.util.Objects; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import org.team1540.rooster.functional.Output; /** * {@link Output} to pass drive commands to Talon SRX and Victor SPX motor controllers. For output diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/DriveData.java b/src/main/java/org/team1540/rooster/drive/pipeline/DriveData.java index 68367ebc..b221ee54 100644 --- a/src/main/java/org/team1540/rooster/drive/pipeline/DriveData.java +++ b/src/main/java/org/team1540/rooster/drive/pipeline/DriveData.java @@ -2,6 +2,8 @@ import java.util.Objects; import java.util.OptionalDouble; +import java.util.function.Function; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; /** @@ -43,6 +45,14 @@ public class DriveData { @NotNull public final OptionalDouble additionalFeedForward; + /** + * Create a new {@code DriveData} with all fields empty. + */ + public DriveData() { + this(OptionalDouble.empty(), OptionalDouble.empty(), OptionalDouble.empty(), + OptionalDouble.empty()); + } + /** * Create a new {@code DriveData} with all fields empty except for the provided velocity. * @@ -74,6 +84,182 @@ public DriveData(@NotNull OptionalDouble position, @NotNull OptionalDouble veloc this.additionalFeedForward = Objects.requireNonNull(additionalFeedForward); } + /** + * Creates a copy of this {@code DriveData} with a different {@link #position} value (all other + * fields remain the same). + * + * @param position The new value for {@link #position}. + * @return A new {@code DriveData} as described above. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public DriveData withPosition(double position) { + return new DriveData(OptionalDouble.of(position), velocity, acceleration, + additionalFeedForward); + } + + /** + * Creates a copy of this {@code DriveData} with a different {@link #velocity} value (all other + * fields remain the same). + * + * @param velocity The new value for {@link #velocity}. + * @return A new {@code DriveData} as described above. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public DriveData withVelocity(double velocity) { + return new DriveData(position, OptionalDouble.of(velocity), acceleration, + additionalFeedForward); + } + + /** + * Creates a copy of this {@code DriveData} with a different {@link #acceleration} value (all + * other fields remain the same). + * + * @param acceleration The new value for {@link #acceleration}. + * @return A new {@code DriveData} as described above. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public DriveData withAcceleration(double acceleration) { + return new DriveData(position, velocity, OptionalDouble.of(acceleration), + additionalFeedForward); + } + + /** + * Creates a copy of this {@code DriveData} with a different {@link #additionalFeedForward} value + * (all other fields remain the same). + * + * @param additionalFeedForward The new value for {@link #additionalFeedForward}. + * @return A new {@code DriveData} as described above. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public DriveData withAdditionalFeedForward(double additionalFeedForward) { + return new DriveData(position, velocity, acceleration, + OptionalDouble.of(additionalFeedForward)); + } + + /** + * Creates a copy of this {@code DriveData} with a modified {@link #position}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current {@link + * #position} and returns a new {@link #position}. + * @return A copy of this {@code DriveData} with all fields identical except for the {@link + * #position} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public DriveData modifyPosition(@NotNull Function function) { + return new DriveData(function.apply(position), velocity, acceleration, additionalFeedForward); + } + + /** + * Creates a copy of this {@code DriveData} with a modified {@link #velocity}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current {@link + * #velocity} and returns a new {@link #velocity}. + * @return A copy of this {@code DriveData} with all fields identical except for the {@link + * #velocity} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public DriveData modifyVelocity(@NotNull Function function) { + return new DriveData(position, function.apply(velocity), acceleration, additionalFeedForward); + } + + /** + * Creates a copy of this {@code DriveData} with a modified {@link #acceleration}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current {@link + * #acceleration} and returns a new {@link #acceleration}. + * @return A copy of this {@code DriveData} with all fields identical except for the {@link + * #acceleration} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public DriveData modifyAcceleration(@NotNull Function function) { + return new DriveData(position, velocity, function.apply(acceleration), additionalFeedForward); + } + + /** + * Creates a copy of this {@code DriveData} with a modified {@link #additionalFeedForward}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current {@link + * #additionalFeedForward} and returns a new {@link #additionalFeedForward}. + * @return A copy of this {@code DriveData} with all fields identical except for the {@link + * #additionalFeedForward} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public DriveData modifyAdditionalFeedForward( + @NotNull Function function) { + return new DriveData(position, velocity, acceleration, function.apply(additionalFeedForward)); + } + + /** + * Adds the provided value to this {@code DriveData}'s {@link #position}. If there is already a + * value present in {@link #position}, the returned {@code DriveData}'s {@link #position} will be + * equal to the sum of that value plus the parameter; otherwise, the {@link #position} will be + * equal to the value of the provided position parameter. + * + * @param position The position value to add. + * @return A new {@link DriveData} as described above. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public DriveData plusPosition(double position) { + return modifyPosition((old) -> OptionalDouble.of(old.orElse(0) + position)); + } + + + /** + * Adds the provided value to this {@code DriveData}'s {@link #velocity}. If there is already a + * value present in {@link #velocity}, the returned {@code DriveData}'s {@link #velocity} will be + * equal to the sum of that value plus the parameter; otherwise, the {@link #velocity} will be + * equal to the value of the provided velocity parameter. + * + * @param velocity The velocity value to add. + * @return A new {@link DriveData} as described above. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public DriveData plusVelocity(double velocity) { + return modifyVelocity((old) -> OptionalDouble.of(old.orElse(0) + velocity)); + } + + /** + * Adds the provided value to this {@code DriveData}'s {@link #acceleration}. If there is already + * a value present in {@link #acceleration}, the returned {@code DriveData}'s {@link + * #acceleration} will be equal to the sum of that value plus the parameter; otherwise, the {@link + * #acceleration} will be equal to the value of the provided acceleration parameter. + * + * @param acceleration The acceleration value to add. + * @return A new {@link DriveData} as described above. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public DriveData plusAcceleration(double acceleration) { + return modifyAcceleration((old) -> OptionalDouble.of(old.orElse(0) + acceleration)); + } + + /** + * Adds the provided value to this {@code DriveData}'s {@link #additionalFeedForward}. If there is + * already a value present in {@link #additionalFeedForward}, the returned {@code DriveData}'s + * {@link #additionalFeedForward} will be equal to the sum of that value plus the parameter; + * otherwise, the {@link #additionalFeedForward} will be equal to the value of the provided + * additionalFeedForward parameter. + * + * @param additionalFeedForward The additionalFeedForward value to add. + * @return A new {@link DriveData} as described above. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public DriveData plusAdditionalFeedForward(double additionalFeedForward) { + return modifyAdditionalFeedForward( + (old) -> OptionalDouble.of(old.orElse(0) + additionalFeedForward)); + } + @Override public String toString() { return (position.isPresent() ? "position " + position.getAsDouble() : "") @@ -82,4 +268,25 @@ public String toString() { + (additionalFeedForward.isPresent() ? ", feedforward " + additionalFeedForward .getAsDouble() : ""); } + + @Override + @Contract(value = "null -> false", pure = true) + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DriveData)) { + return false; + } + DriveData driveData = (DriveData) o; + return position.equals(driveData.position) && + velocity.equals(driveData.velocity) && + acceleration.equals(driveData.acceleration) && + additionalFeedForward.equals(driveData.additionalFeedForward); + } + + @Override + public int hashCode() { + return Objects.hash(position, velocity, acceleration, additionalFeedForward); + } } diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/FeedForwardProcessor.java b/src/main/java/org/team1540/rooster/drive/pipeline/FeedForwardProcessor.java index e2032e89..95f9f3e0 100644 --- a/src/main/java/org/team1540/rooster/drive/pipeline/FeedForwardProcessor.java +++ b/src/main/java/org/team1540/rooster/drive/pipeline/FeedForwardProcessor.java @@ -1,8 +1,8 @@ package org.team1540.rooster.drive.pipeline; -import java.util.OptionalDouble; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import org.team1540.rooster.functional.Processor; /** * {@link Processor} to apply an Oblarg-style @@ -19,9 +19,11 @@ public class FeedForwardProcessor implements Processor *

  • The feed-forward starts at 0.
  • *
  • The product of the velocity (if present) and the velocity feed-forward is added.
  • - *
  • The product of the acceleration (if present) and the acceleration feed-forward is added.
  • + *
  • The product of the acceleration (if present) and the acceleration feed-forward is + * added.
  • *
  • If the velocity is present and nonzero, the throttle bump (with the sign of the * velocity) is added.
  • * @@ -55,20 +58,8 @@ private double getThrottle(double wantedSpeed, double wantedAccel) { @Override @NotNull public TankDriveData apply(@NotNull TankDriveData command) { - return new TankDriveData( - new DriveData(command.left.position, - command.left.velocity, - command.left.acceleration, - OptionalDouble.of(command.left.additionalFeedForward.orElse(0) - + getThrottle(command.left.velocity.orElse(0), - command.left.acceleration.orElse(0)))), - new DriveData(command.right.position, - command.right.velocity, - command.right.acceleration, - OptionalDouble.of(command.right.additionalFeedForward.orElse(0) - + getThrottle(command.right.velocity.orElse(0), - command.right.acceleration.orElse(0)))), - command.heading, - command.turningRate); + return command.plusAdditionalFeedForwards( + getThrottle(command.left.velocity.orElse(0), command.left.acceleration.orElse(0)), + getThrottle(command.right.velocity.orElse(0), command.right.acceleration.orElse(0))); } } diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/FeedForwardToVelocityProcessor.java b/src/main/java/org/team1540/rooster/drive/pipeline/FeedForwardToVelocityProcessor.java new file mode 100644 index 00000000..4afe9b48 --- /dev/null +++ b/src/main/java/org/team1540/rooster/drive/pipeline/FeedForwardToVelocityProcessor.java @@ -0,0 +1,66 @@ +package org.team1540.rooster.drive.pipeline; + +import java.util.OptionalDouble; +import java.util.function.DoubleSupplier; +import org.team1540.rooster.functional.Processor; + +/** + * {@link Processor} that converts a feed-forward/throttle into a velocity setpoint. This processor + * multiplies each feed-forward by the maximum velocity of the robot. This is useful when running + * closed-loop teleop drive code (as joystick inputs usually provide feed-forwards). + */ +public class FeedForwardToVelocityProcessor implements Processor { + + private DoubleSupplier maxVelocitySupplier; + private boolean clearFeedForwards; + + /** + * Creates a new {@code FeedForwardToVelocityProcessor} that clears feed-forwards in the returned + * data. + * + * @param maxVelocity The maximum velocity of the robot. + */ + public FeedForwardToVelocityProcessor(double maxVelocity) { + this(maxVelocity, true); + } + + /** + * Creates a new {@code FeedForwardToVelocityProcessor}. + * + * @param maxVelocity The maximum velocity of the robot. + * @param clearFeedForwards Whether to set the feed-forwards in the {@link TankDriveData} to empty + * {@link OptionalDouble OptionalDoubles}. If {@code true}, the value will be cleared; if {@code + * false}, it will be passed through as-is. + */ + public FeedForwardToVelocityProcessor(double maxVelocity, boolean clearFeedForwards) { + this(() -> maxVelocity, clearFeedForwards); + } + + /** + * Creates a new {@code FeedForwardToVelocityProcessor}. + * + * @param maxVelSupplier A supplier that supplies the maximum velocity of the robot. + * @param clearFeedForwards Whether to set the feed-forwards in the {@link TankDriveData} to empty + * {@link OptionalDouble OptionalDoubles}. If {@code true}, the value will be cleared; if {@code + * false}, it will be passed through as-is. + */ + public FeedForwardToVelocityProcessor(DoubleSupplier maxVelSupplier, boolean clearFeedForwards) { + this.maxVelocitySupplier = maxVelSupplier; + this.clearFeedForwards = clearFeedForwards; + } + + @Override + public TankDriveData apply(TankDriveData data) { + double maxVelocity = maxVelocitySupplier.getAsDouble(); + return new TankDriveData( + new DriveData(data.left.position, + OptionalDouble.of(data.left.additionalFeedForward.orElse(0) * maxVelocity), + data.left.acceleration, + clearFeedForwards ? OptionalDouble.empty() : data.left.additionalFeedForward), + new DriveData(data.right.position, + OptionalDouble.of(data.right.additionalFeedForward.orElse(0) * maxVelocity), + data.right.acceleration, + clearFeedForwards ? OptionalDouble.empty() : data.right.additionalFeedForward), + data.heading, data.turningRate); + } +} diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/HeadingPIDProcessor.java b/src/main/java/org/team1540/rooster/drive/pipeline/HeadingPIDProcessor.java new file mode 100644 index 00000000..edaba266 --- /dev/null +++ b/src/main/java/org/team1540/rooster/drive/pipeline/HeadingPIDProcessor.java @@ -0,0 +1,114 @@ +package org.team1540.rooster.drive.pipeline; + +import java.util.function.DoubleSupplier; + +/** + * A {@link PIDProcessor} for maintaining a robot's heading. + */ +public class HeadingPIDProcessor extends PIDProcessor { + + private DoubleSupplier headingSupplier; + private boolean outputToPosition; + private boolean invertSides; + + /** + * Create a new {@code HeadingPIDProcessor} without inverted sides that outputs to feed-forward + * setpoints. + * + * @param p The P coefficient. + * @param headingSupplier A supplier that returns the current heading, in radians from -π to + * π. + */ + public HeadingPIDProcessor(double p, DoubleSupplier headingSupplier) { + this(p, 0, headingSupplier); + } + + /** + * Create a new {@code HeadingPIDProcessor} without inverted sides that outputs to feed-forward + * setpoints. + * + * @param p The P coefficient. + * @param i The I coefficient. + * @param headingSupplier A supplier that returns the current heading, in radians from -π to + * π. + */ + public HeadingPIDProcessor(double p, double i, DoubleSupplier headingSupplier) { + this(p, i, 0, headingSupplier); + } + + /** + * Create a new {@code HeadingPIDProcessor} without inverted sides that outputs to feed-forward + * setpoints. + * + * @param p The P coefficient. + * @param i The I coefficient. + * @param d The D coefficient. + * @param headingSupplier A supplier that returns the current heading, in radians from -π to + * π. + */ + public HeadingPIDProcessor(double p, double i, double d, DoubleSupplier headingSupplier) { + this(p, i, d, headingSupplier, false); + } + + /** + * Create a new {@code HeadingPIDProcessor} without inverted sides. + * + * @param p The P coefficient. + * @param i The I coefficient. + * @param d The D coefficient. + * @param headingSupplier A supplier that returns the current heading, in radians from -π to + * π. + * @param outputToPosition Whether to output to the position setpoints (as opposed to the + * feed-forward setpoints.) + */ + public HeadingPIDProcessor(double p, double i, double d, DoubleSupplier headingSupplier, + boolean outputToPosition) { + this(p, i, d, headingSupplier, outputToPosition, false); + } + + /** + * Create a new {@code HeadingPIDProcessor}. + * + * @param p The P coefficient. + * @param i The I coefficient. + * @param d The D coefficient. + * @param headingSupplier A supplier that returns the current heading, in radians from -π to + * π. + * @param outputToPosition Whether to output to the position setpoints (as opposed to the + * feed-forward setpoints.) + * @param invertSides Whether to invert the output sides. (If {@code true}, the loop output will + * be added to the left side and subtracted from the right, and vice versa.) + */ + public HeadingPIDProcessor(double p, double i, double d, DoubleSupplier headingSupplier, + boolean outputToPosition, boolean invertSides) { + super(p, i, d); + this.headingSupplier = headingSupplier; + this.outputToPosition = outputToPosition; + this.invertSides = invertSides; + } + + @Override + protected double getError(TankDriveData data) { + if (data.heading.isPresent()) { + double heading = headingSupplier.getAsDouble(); + double headingTarget = data.heading.getAsDouble(); + + // basically magic https://stackoverflow.com/a/2007279 + return Math.atan2(Math.sin(heading - headingTarget), Math.cos(heading - headingTarget)); + } else { + return 0; + } + } + + @Override + protected TankDriveData createOutput(TankDriveData data, double loopOutput) { + // multiplying the output by -1 effectively flips the sides + loopOutput *= invertSides ? -1 : 1; + + if (outputToPosition) { + return data.plusPositions(-loopOutput, loopOutput); + } else { + return data.plusAdditionalFeedForwards(-loopOutput, loopOutput); + } + } +} diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/HeadingTransformProcessor.java b/src/main/java/org/team1540/rooster/drive/pipeline/HeadingTransformProcessor.java new file mode 100644 index 00000000..161ce777 --- /dev/null +++ b/src/main/java/org/team1540/rooster/drive/pipeline/HeadingTransformProcessor.java @@ -0,0 +1,54 @@ +package org.team1540.rooster.drive.pipeline; + +import org.team1540.rooster.functional.Processor; + +/** + * {@link Processor} to convert headings on [0,2π] to [-π, π] and vice versa. (This class + * also supports degrees by passing {@code true} as the second argument to {@link + * #HeadingTransformProcessor(boolean, boolean)}. + * + * This class is a {@link Processor} that uses {@link TankDriveData}. All fields are passed through + * as-is except for the heading field which is converted if it is present. + */ +public class HeadingTransformProcessor implements Processor { + + private boolean outputPositive; + private boolean radians; + + /** + * Create a new {@code HeadingTransformProcessor} that uses radians. + * + * @param outputPositive Whether to output values in the range [0,2π] (i.e. [0,360]). If {@code + * true}, creates a {@code HeadingTransformProcessor} to convert angles on [-π, π] to angles + * on [0,2π]; if {@code false}, creates one that does the reverse. + */ + public HeadingTransformProcessor(boolean outputPositive) { + this(outputPositive, true); + } + + /** + * Create a new {@code HeadingTransformProcessor}. + * + * @param outputPositive Whether to output values in the range [0,2π] (or [0,360]). If {@code + * true}, creates a {@code HeadingTransformProcessor} to convert angles on [-π, π] to angles + * on [0,2π]; if {@code false}, creates one that does the reverse. + * @param radians Whether to use radians. If {@code true}, assumes inputs are in radians; + * otherwise, uses degrees. + */ + public HeadingTransformProcessor(boolean outputPositive, boolean radians) { + this.outputPositive = outputPositive; + this.radians = radians; + } + + @Override + public TankDriveData apply(TankDriveData data) { + if (data.heading.isPresent()) { + double heading = data.heading.getAsDouble(); + double halfOfCircle = radians ? Math.PI : 180; + double processed = heading + ((outputPositive ? 1 : -1) * halfOfCircle); + return data.withHeading(processed); + } else { + return data; + } + } +} diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/PIDProcessor.java b/src/main/java/org/team1540/rooster/drive/pipeline/PIDProcessor.java new file mode 100644 index 00000000..307e428b --- /dev/null +++ b/src/main/java/org/team1540/rooster/drive/pipeline/PIDProcessor.java @@ -0,0 +1,115 @@ +package org.team1540.rooster.drive.pipeline; + +import org.team1540.rooster.functional.Processor; + +/** + * Processor to execute a generic PID loop. + * + * @param The input type of the processor. + * @param The output type of the processor. + */ +public abstract class PIDProcessor implements Processor { + + private double p, i, d; + + private double iAccum; + + private double lastError; + + private long lastTime = -1; + + /** + * Create a new {@code PIDProcessor}. + * + * @param p The P coefficient. + * @param i The I coefficient. + * @param d The D coefficient. + */ + protected PIDProcessor(double p, double i, double d) { + this.p = p; + this.i = i; + this.d = d; + } + + /** + * Extract the PID loop target from the data passed to the processor, and calculate the current + * error. + * + * @param data The data instance that was passed to the processor's {@link #apply(Object) apply()} + * method. + * @return The error. + */ + protected abstract double getError(I data); + + /** + * Create the processor output. + * + * @param data The data instance that was passed to the processor's {@link #apply(Object) apply()} + * method. + * @param loopOutput The output of the PID loop + * @return The processor's output; this will be returned from the the processor's {@link + * #apply(Object) apply()} method. + */ + protected abstract O createOutput(I data, double loopOutput); + + @Override + public O apply(I input) { + double output = 0; + double error = getError(input); + + // p gain + output += error * p; + + if (lastTime != -1) { + double dt = (System.currentTimeMillis() - lastTime) / 1000.0; + + // i gain + iAccum += error * dt; + output += i * iAccum; + + // d gain + double dError = (error - lastError) / dt; + output += d * dError; + } + + lastError = error; + lastTime = System.currentTimeMillis(); + + return createOutput(input, output); + } + + + /** + * Gets the current value of the integral accumulator. + * + * @return The integral accumulator. If the PID loop has not yet been run (i.e. {@link + * #apply(Object) apply()} has not yet been called since instantiation/the last call to {@link + * #reset()}), returns 0. + */ + public double getIAccum() { + return iAccum; + } + + /** + * Gets the closed-loop error from the last run of the PID loop. + * + * @return The closed-loop error. If the PID loop has not yet been run (i.e. {@link #apply(Object) + * apply()} has not yet been called since instantiation/the last call to {@link #reset()}), + * returns 0. + */ + public double getError() { + return lastError; + } + + /** + * Resets the PID loop. This clears the integral accumulator and error values, and should be + * called if the {@code TurningRateClosedLoopProcessor} is being reused for multiple discrete + * occasions (i.e. executions of different motion profile segments). Calling this is functionally + * equivalent to creating a new processor instance. + */ + public void reset() { + iAccum = 0; + lastError = 0; + lastTime = -1; + } +} diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/ProfileInput.java b/src/main/java/org/team1540/rooster/drive/pipeline/ProfileInput.java new file mode 100644 index 00000000..2715e19c --- /dev/null +++ b/src/main/java/org/team1540/rooster/drive/pipeline/ProfileInput.java @@ -0,0 +1,85 @@ +package org.team1540.rooster.drive.pipeline; + +import edu.wpi.first.wpilibj.Timer; +import java.util.OptionalDouble; +import org.jetbrains.annotations.NotNull; +import org.team1540.rooster.functional.Input; +import org.team1540.rooster.motionprofiling.MotionProfile; +import org.team1540.rooster.motionprofiling.MotionProfile.Point; + +/** + * Class to get commanded drive values from a set of motion profiles. + * + * This class is non-reusable; the first call to {@link #get()} begins a timer to determine where + * the input is in the profile. To execute multiple profiles, create multiple {@code + * ProfileInputs}. + */ +public class ProfileInput implements Input { + + private MotionProfile left; + private MotionProfile right; + + private Timer timer = new Timer(); + + private boolean finished; + + /** + * Create a new {@code ProfileInput}. + * + * @param left The left-side profile to execute. + * @param right The right-side profile to execute. + */ + public ProfileInput(@NotNull MotionProfile left, @NotNull MotionProfile right) { + this.left = left; + this.right = right; + } + + @NotNull + private Point getCurrentSegment(@NotNull MotionProfile trajectory, double currentTime) { + // Start from the current time and find the closest point. + int startIndex = Math.toIntExact(Math.round(currentTime / trajectory.get(0).dt)); + + int length = trajectory.size(); + int index = startIndex; + if (startIndex >= length - 1) { + index = length - 1; + finished = true; + } + return trajectory.get(index); + } + + @Override + public TankDriveData get() { + if (timer.get() <= 0) { + timer.start(); + } + + double timeValue = timer.get(); + + Point leftPoint = getCurrentSegment(left, timeValue); + Point rightPoint = getCurrentSegment(right, timeValue); + + return new TankDriveData( + new DriveData( + OptionalDouble.of(leftPoint.position), + OptionalDouble.of(leftPoint.velocity), + OptionalDouble.of(leftPoint.acceleration), + OptionalDouble.empty()), + new DriveData( + OptionalDouble.of(rightPoint.position), + OptionalDouble.of(rightPoint.velocity), + OptionalDouble.of(rightPoint.acceleration), + OptionalDouble.empty()), + OptionalDouble.of(leftPoint.heading), + OptionalDouble.empty()); + } + + /** + * Returns whether the end of the profile has been reached. + * + * @return {@code true} if the end of the profile has been reached, {@code false} otherwise. + */ + public boolean isFinished() { + return finished; + } +} diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/SimpleJoystickInput.java b/src/main/java/org/team1540/rooster/drive/pipeline/SimpleJoystickInput.java index 49c7558f..fc5f19d1 100644 --- a/src/main/java/org/team1540/rooster/drive/pipeline/SimpleJoystickInput.java +++ b/src/main/java/org/team1540/rooster/drive/pipeline/SimpleJoystickInput.java @@ -1,8 +1,8 @@ package org.team1540.rooster.drive.pipeline; import edu.wpi.first.wpilibj.Joystick; -import java.util.OptionalDouble; import org.team1540.rooster.Utilities; +import org.team1540.rooster.functional.Input; /** * Simple tank-style input from a WPILib {@link Joystick}. The left and right joysticks are used to @@ -42,22 +42,7 @@ public TankDriveData get() { Utilities.invertIf(invertRight, joystick.getRawAxis(rightAxis)), deadzone ) + triggerValue, 1); - return new TankDriveData( - new DriveData( - OptionalDouble.empty(), - OptionalDouble.empty(), - OptionalDouble.empty(), - OptionalDouble.of(leftThrottle) - ), - new DriveData( - OptionalDouble.empty(), - OptionalDouble.empty(), - OptionalDouble.empty(), - OptionalDouble.of(rightThrottle) - ), - OptionalDouble.empty(), - OptionalDouble.empty() - ); + return new TankDriveData().withAdditionalFeedForwards(leftThrottle, rightThrottle); } /** diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/TankDriveData.java b/src/main/java/org/team1540/rooster/drive/pipeline/TankDriveData.java index ee169a1c..35174659 100644 --- a/src/main/java/org/team1540/rooster/drive/pipeline/TankDriveData.java +++ b/src/main/java/org/team1540/rooster/drive/pipeline/TankDriveData.java @@ -2,6 +2,8 @@ import java.util.Objects; import java.util.OptionalDouble; +import java.util.function.Function; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; /** @@ -39,6 +41,619 @@ public class TankDriveData { @NotNull public final OptionalDouble turningRate; + /** + * Creates a new {@code TankDriveData} with all fields empty. + */ + public TankDriveData() { + this(new DriveData(), new DriveData(), OptionalDouble.empty(), OptionalDouble.empty()); + } + + /** + * Creates a copy of this {@code TankDriveData} with a modified {@link #heading}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current {@link #heading} + * and returns a new {@link #heading}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the {@link + * #heading} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData modifyHeading(@NotNull Function function) { + return new TankDriveData(left, right, function.apply(heading), turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} with a modified {@link #turningRate}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current {@link + * #turningRate} and returns a new {@link #turningRate}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the {@link + * #turningRate} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData modifyTurningRate( + @NotNull Function function) { + return new TankDriveData(left, right, heading, function.apply(turningRate)); + } + + /** + * Creates a copy of this {@code TankDriveData} with a modified left {@link DriveData#position + * position}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current left {@link + * DriveData#position position} and returns a new {@link DriveData#position position}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the left + * {@link DriveData#position position} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData modifyLeftPosition( + @NotNull Function function) { + return new TankDriveData(left.modifyPosition(function), right, heading, turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} with a modified left {@link DriveData#velocity + * velocity}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current left {@link + * DriveData#velocity velocity} and returns a new {@link DriveData#velocity velocity}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the left + * {@link DriveData#velocity velocity} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData modifyLeftVelocity( + @NotNull Function function) { + return new TankDriveData(left.modifyVelocity(function), right, heading, turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} with a modified left {@link DriveData#acceleration + * acceleration}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current left {@link + * DriveData#acceleration acceleration} and returns a new {@link DriveData#acceleration + * acceleration}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the left + * {@link DriveData#acceleration acceleration} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData modifyLeftAcceleration( + @NotNull Function function) { + return new TankDriveData(left.modifyAcceleration(function), right, heading, turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} with a modified left {@link + * DriveData#additionalFeedForward additionalFeedForward}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current left {@link + * DriveData#additionalFeedForward additionalFeedForward} and returns a new {@link + * DriveData#additionalFeedForward additionalFeedForward}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the left + * {@link DriveData#additionalFeedForward additionalFeedForward} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData modifyLeftAdditionalFeedForward( + @NotNull Function function) { + return new TankDriveData(left.modifyAdditionalFeedForward(function), right, heading, + turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} with a modified right {@link DriveData#position + * position}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current right {@link + * DriveData#position position} and returns a new {@link DriveData#position position}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the right + * {@link DriveData#position position} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData modifyRightPosition( + @NotNull Function function) { + return new TankDriveData(left, right.modifyPosition(function), heading, turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} with a modified right {@link DriveData#velocity + * velocity}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current right {@link + * DriveData#velocity velocity} and returns a new {@link DriveData#velocity velocity}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the right + * {@link DriveData#velocity velocity} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData modifyRightVelocity( + @NotNull Function function) { + return new TankDriveData(left, right.modifyVelocity(function), heading, turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} with a modified right {@link + * DriveData#acceleration acceleration}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current right {@link + * DriveData#acceleration acceleration} and returns a new {@link DriveData#acceleration + * acceleration}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the right + * {@link DriveData#acceleration acceleration} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData modifyRightAcceleration( + @NotNull Function function) { + return new TankDriveData(left, right.modifyAcceleration(function), heading, turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} with a modified right {@link + * DriveData#additionalFeedForward additionalFeedForward}. + * + * @param function A {@link Function} that takes this {@code DriveData}'s current right {@link + * DriveData#additionalFeedForward additionalFeedForward} and returns a new {@link + * DriveData#additionalFeedForward additionalFeedForward}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the right + * {@link DriveData#additionalFeedForward additionalFeedForward} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData modifyRightAdditionalFeedForward( + @NotNull Function function) { + return new TankDriveData(left, right.modifyAdditionalFeedForward(function), heading, + turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} with modified left and right positions. + * + * @param lfunc A {@link Function} that takes this {@code DriveData}'s current left {@link + * DriveData#position position} and returns a new {@link DriveData#position position}. + * @param rfunc A {@link Function} that takes this {@code DriveData}'s current right {@link + * DriveData#position position} and returns a new {@link DriveData#position position}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the left and + * right {@link DriveData#additionalFeedForward additionalFeedForward} fields. + */ + @NotNull + @Contract(value = "_, _ -> new", pure = true) + public TankDriveData modifyPosition(@NotNull Function lfunc, + @NotNull Function rfunc) { + return new TankDriveData(left.modifyPosition(lfunc), right.modifyPosition(rfunc), heading, + turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} with modified left and right velocitys. + * + * @param lfunc A {@link Function} that takes this {@code DriveData}'s current left {@link + * DriveData#velocity velocity} and returns a new {@link DriveData#velocity velocity}. + * @param rfunc A {@link Function} that takes this {@code DriveData}'s current right {@link + * DriveData#velocity velocity} and returns a new {@link DriveData#velocity velocity}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the left and + * right {@link DriveData#additionalFeedForward additionalFeedForward} fields. + */ + @NotNull + @Contract(value = "_, _ -> new", pure = true) + public TankDriveData modifyVelocity(@NotNull Function lfunc, + @NotNull Function rfunc) { + return new TankDriveData(left.modifyVelocity(lfunc), right.modifyVelocity(rfunc), heading, + turningRate); + } + + + /** + * Creates a copy of this {@code TankDriveData} with modified left and right accelerations. + * + * @param lfunc A {@link Function} that takes this {@code DriveData}'s current left {@link + * DriveData#acceleration acceleration} and returns a new {@link DriveData#acceleration + * acceleration}. + * @param rfunc A {@link Function} that takes this {@code DriveData}'s current right {@link + * DriveData#acceleration acceleration} and returns a new {@link DriveData#acceleration + * acceleration}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the left and + * right {@link DriveData#additionalFeedForward additionalFeedForward} fields. + */ + @NotNull + @Contract(value = "_, _ -> new", pure = true) + public TankDriveData modifyAcceleration(@NotNull Function lfunc, + @NotNull Function rfunc) { + return new TankDriveData(left.modifyAcceleration(lfunc), right.modifyAcceleration(rfunc), + heading, + turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} with modified left and right feed-forwards. + * + * @param lfunc A {@link Function} that takes this {@code DriveData}'s current left {@link + * DriveData#additionalFeedForward additionalFeedForward} and returns a new {@link + * DriveData#additionalFeedForward additionalFeedForward}. + * @param rfunc A {@link Function} that takes this {@code DriveData}'s current right {@link + * DriveData#additionalFeedForward additionalFeedForward} and returns a new {@link + * DriveData#additionalFeedForward additionalFeedForward}. + * @return A copy of this {@code TankDriveData} with all fields identical except for the left and + * right {@link DriveData#additionalFeedForward additionalFeedForward} fields. + */ + @NotNull + @Contract(value = "_, _ -> new", pure = true) + public TankDriveData modifyAdditionalFeedForward( + @NotNull Function lfunc, + @NotNull Function rfunc) { + return new TankDriveData(left.modifyAdditionalFeedForward(lfunc), + right.modifyAdditionalFeedForward(rfunc), heading, + turningRate); + } + + /** + * Adds the provided value to this {@code TankDriveData}'s {@link #heading}. If there is already a + * value present in {@link #heading}, the returned {@code TankDriveData}'s {@link #heading} will + * be equal to the sum of that value plus the parameter; otherwise, the left {@link #heading} will + * be equal to the value of the provided heading parameter. + * + * @param heading The heading to add. + * @return A new {@code TankDriveData} with all fields identical except for the left {@link + * #heading} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData plusHeading(double heading) { + return modifyHeading((old) -> OptionalDouble.of(old.orElse(0) + heading)); + } + + /** + * Adds the provided value to this {@code TankDriveData}'s {@link #turningRate}. If there is + * already a value present in {@link #turningRate}, the returned {@code TankDriveData}'s {@link + * #turningRate} will be equal to the sum of that value plus the parameter; otherwise, the left + * {@link #turningRate} will be equal to the value of the provided turningRate parameter. + * + * @param turningRate The turningRate to add. + * @return A new {@code TankDriveData} with all fields identical except for the left {@link + * #turningRate} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData plusTurningRate(double turningRate) { + return modifyTurningRate((old) -> OptionalDouble.of(old.orElse(0) + turningRate)); + } + + + /** + * Adds the provided value to this {@code TankDriveData}'s left {@link DriveData#position + * position}. If there is already a value present in {@link #left left.}{@link DriveData#position + * position}, the returned {@code TankDriveData}'s left {@link DriveData#position position} will + * be equal to the sum of that value plus the parameter; otherwise, the left {@link + * DriveData#position position} will be equal to the value of the provided position parameter. + * + * @param position The position to add. + * @return A new {@code TankDriveData} with all fields identical except for the left {@link + * DriveData#position position} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData plusLeftPosition(double position) { + return new TankDriveData(left.plusPosition(position), right, heading, turningRate); + } + + /** + * Adds the provided value to this {@code TankDriveData}'s left {@link DriveData#velocity + * velocity}. If there is already a value present in {@link #left left.}{@link DriveData#velocity + * velocity}, the returned {@code TankDriveData}'s left {@link DriveData#velocity velocity} will + * be equal to the sum of that value plus the parameter; otherwise, the left {@link + * DriveData#velocity velocity} will be equal to the value of the provided velocity parameter. + * + * @param velocity The velocity to add. + * @return A new {@code TankDriveData} with all fields identical except for the left {@link + * DriveData#velocity velocity} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData plusLeftVelocity(double velocity) { + return new TankDriveData(left.plusVelocity(velocity), right, heading, turningRate); + } + + + /** + * Adds the provided value to this {@code TankDriveData}'s left {@link DriveData#acceleration + * acceleration}. If there is already a value present in {@link #left left.}{@link + * DriveData#acceleration acceleration}, the returned {@code TankDriveData}'s left {@link + * DriveData#acceleration acceleration} will be equal to the sum of that value plus the parameter; + * otherwise, the left {@link DriveData#acceleration acceleration} will be equal to the value of + * the provided acceleration parameter. + * + * @param acceleration The acceleration to add. + * @return A new {@code TankDriveData} with all fields identical except for the left {@link + * DriveData#acceleration acceleration} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData plusLeftAcceleration(double acceleration) { + return new TankDriveData(left.plusAcceleration(acceleration), right, heading, turningRate); + } + + /** + * Adds the provided value to this {@code TankDriveData}'s left {@link + * DriveData#additionalFeedForward additionalFeedForward}. If there is already a value present in + * {@link #left left.}{@link DriveData#additionalFeedForward additionalFeedForward}, the returned + * {@code TankDriveData}'s left {@link DriveData#additionalFeedForward additionalFeedForward} will + * be equal to the sum of that value plus the parameter; otherwise, the left {@link + * DriveData#additionalFeedForward additionalFeedForward} will be equal to the value of the + * provided additionalFeedForward parameter. + * + * @param additionalFeedForward The additionalFeedForward to add. + * @return A new {@code TankDriveData} with all fields identical except for the left {@link + * DriveData#additionalFeedForward additionalFeedForward} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData plusLeftAdditionalFeedForward(double additionalFeedForward) { + return new TankDriveData(left.plusAdditionalFeedForward(additionalFeedForward), right, heading, + turningRate); + } + + + /** + * Adds the provided value to this {@code TankDriveData}'s right {@link DriveData#position + * position}. If there is already a value present in {@link #right right.}{@link + * DriveData#position position}, the returned {@code TankDriveData}'s right {@link + * DriveData#position position} will be equal to the sum of that value plus the parameter; + * otherwise, the right {@link DriveData#position position} will be equal to the value of the + * provided position parameter. + * + * @param position The position to add. + * @return A new {@code TankDriveData} with all fields identical except for the right {@link + * DriveData#position position} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData plusRightPosition(double position) { + return new TankDriveData(left, right.plusPosition(position), heading, turningRate); + } + + /** + * Adds the provided value to this {@code TankDriveData}'s right {@link DriveData#velocity + * velocity}. If there is already a value present in {@link #right right.}{@link + * DriveData#velocity velocity}, the returned {@code TankDriveData}'s right {@link + * DriveData#velocity velocity} will be equal to the sum of that value plus the parameter; + * otherwise, the right {@link DriveData#velocity velocity} will be equal to the value of the + * provided velocity parameter. + * + * @param velocity The velocity to add. + * @return A new {@code TankDriveData} with all fields identical except for the right {@link + * DriveData#velocity velocity} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData plusRightVelocity(double velocity) { + return new TankDriveData(left, right.plusVelocity(velocity), heading, turningRate); + } + + + /** + * Adds the provided value to this {@code TankDriveData}'s right {@link DriveData#acceleration + * acceleration}. If there is already a value present in {@link #right right.}{@link + * DriveData#acceleration acceleration}, the returned {@code TankDriveData}'s right {@link + * DriveData#acceleration acceleration} will be equal to the sum of that value plus the parameter; + * otherwise, the right {@link DriveData#acceleration acceleration} will be equal to the value of + * the provided acceleration parameter. + * + * @param acceleration The acceleration to add. + * @return A new {@code TankDriveData} with all fields identical except for the right {@link + * DriveData#acceleration acceleration} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData plusRightAcceleration(double acceleration) { + return new TankDriveData(left, right.plusAcceleration(acceleration), heading, turningRate); + } + + /** + * Adds the provided value to this {@code TankDriveData}'s right {@link + * DriveData#additionalFeedForward additionalFeedForward}. If there is already a value present in + * {@link #right right.}{@link DriveData#additionalFeedForward additionalFeedForward}, the + * returned {@code TankDriveData}'s right {@link DriveData#additionalFeedForward + * additionalFeedForward} will be equal to the sum of that value plus the parameter; otherwise, + * the right {@link DriveData#additionalFeedForward additionalFeedForward} will be equal to the + * value of the provided additionalFeedForward parameter. + * + * @param additionalFeedForward The additionalFeedForward to add. + * @return A new {@code TankDriveData} with all fields identical except for the right {@link + * DriveData#additionalFeedForward additionalFeedForward} field. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData plusRightAdditionalFeedForward(double additionalFeedForward) { + return new TankDriveData(left, right.plusAdditionalFeedForward(additionalFeedForward), heading, + turningRate); + } + + /** + * Adds the provided values to this {@code TankDriveData}'s left and right {@link + * DriveData#position position}. If there is already a value present in either {@link + * DriveData#position position}, the returned {@code TankDriveData}'s {@link DriveData#position + * positions} will be equal to the sum of that value plus the parameter; otherwise, the {@link + * DriveData#position position} will be equal to the value of the provided parameter for each + * side. + * + * @param leftPosition The position to add to the left side. + * @param rightPosition The position to add to the right side. + * @return A new {@code TankDriveData} with all fields identical except for the left and right + * {@link DriveData#position position} fields. + */ + @NotNull + @Contract(value = "_, _ -> new", pure = true) + public TankDriveData plusPositions(double leftPosition, double rightPosition) { + return new TankDriveData(left.plusPosition(leftPosition), right.plusPosition(rightPosition), + heading, turningRate); + } + + /** + * Adds the provided values to this {@code TankDriveData}'s left and right {@link + * DriveData#velocity velocity}. If there is already a value present in either {@link + * DriveData#velocity velocity}, the returned {@code TankDriveData}'s {@link DriveData#velocity + * velocities} will be equal to the sum of that value plus the parameter; otherwise, the {@link + * DriveData#velocity velocity} will be equal to the value of the provided parameter for each + * side. + * + * @param leftVelocity The velocity to add to the left side. + * @param rightVelocity The velocity to add to the right side. + * @return A new {@code TankDriveData} with all fields identical except for the left and right + * {@link DriveData#velocity velocity} fields. + */ + @NotNull + @Contract(value = "_, _ -> new", pure = true) + public TankDriveData plusVelocities(double leftVelocity, double rightVelocity) { + return new TankDriveData(left.plusVelocity(leftVelocity), right.plusVelocity(rightVelocity), + heading, turningRate); + } + + /** + * Adds the provided values to this {@code TankDriveData}'s left and right {@link + * DriveData#acceleration acceleration}. If there is already a value present in either {@link + * DriveData#acceleration acceleration}, the returned {@code TankDriveData}'s {@link + * DriveData#acceleration accelerations} will be equal to the sum of that value plus the + * parameter; otherwise, the {@link DriveData#acceleration acceleration} will be equal to the + * value of the provided parameter for each side. + * + * @param leftAcceleration The acceleration to add to the left side. + * @param rightAcceleration The acceleration to add to the right side. + * @return A new {@code TankDriveData} with all fields identical except for the left and right + * {@link DriveData#acceleration acceleration} fields. + */ + @NotNull + @Contract(value = "_, _ -> new", pure = true) + public TankDriveData plusAccelerations(double leftAcceleration, double rightAcceleration) { + return new TankDriveData(left.plusAcceleration(leftAcceleration), + right.plusAcceleration(rightAcceleration), + heading, turningRate); + } + + /** + * Adds the provided values to this {@code TankDriveData}'s left and right {@link + * DriveData#additionalFeedForward additionalFeedForward}. If there is already a value present in + * either {@link DriveData#additionalFeedForward additionalFeedForward}, the returned {@code + * TankDriveData}'s {@link DriveData#additionalFeedForward additionalFeedForwards} will be equal + * to the sum of that value plus the parameter; otherwise, the {@link + * DriveData#additionalFeedForward additionalFeedForward} will be equal to the value of the + * provided parameter for each side. + * + * @param leftAdditionalFeedForward The additionalFeedForward to add to the left side. + * @param rightAdditionalFeedForward The additionalFeedForward to add to the right side. + * @return A new {@code TankDriveData} with all fields identical except for the left and right + * {@link DriveData#additionalFeedForward additionalFeedForward} fields. + */ + @NotNull + @Contract(value = "_, _ -> new", pure = true) + public TankDriveData plusAdditionalFeedForwards(double leftAdditionalFeedForward, + double rightAdditionalFeedForward) { + return new TankDriveData(left.plusAdditionalFeedForward(leftAdditionalFeedForward), + right.plusAdditionalFeedForward(rightAdditionalFeedForward), + heading, turningRate); + } + + + /** + * Creates a copy of this {@code TankDriveData} and sets its {@link #heading}. If there is already + * a value present in {@link #heading}, it will be overwritten. + * + * @param heading The new heading. + * @return A {@code TankDriveData} with all fields identical except for {@link #heading}. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData withHeading(double heading) { + return new TankDriveData(left, right, OptionalDouble.of(heading), turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} and sets its {@link #turningRate}. If there is + * already a value present in {@link #turningRate}, it will be overwritten. + * + * @param turningRate The new turningRate. + * @return A {@code TankDriveData} with all fields identical except for {@link #turningRate}. + */ + @NotNull + @Contract(value = "_ -> new", pure = true) + public TankDriveData withTurningRate(double turningRate) { + return new TankDriveData(left, right, heading, OptionalDouble.of(turningRate)); + } + + /** + * Creates a copy of this {@code TankDriveData} and sets its left and right {@link + * DriveData#position positions}. If there is already a value present in either {@link + * DriveData#position position}, it will be overwritten during copying. + * + * @param leftPosition The position to set on the left side. + * @param rightPosition The position to set on the right side. + * @return A new {@code TankDriveData} with all fields identical except for the left and right + * {@link DriveData#position position} fields. + */ + @NotNull + @Contract(value = "_, _ -> new", pure = true) + public TankDriveData withPositions(double leftPosition, double rightPosition) { + return new TankDriveData(left.withPosition(leftPosition), right.withPosition(rightPosition), + heading, turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} and sets its left and right {@link + * DriveData#velocity velocities}. If there is already a value present in either {@link + * DriveData#velocity velocity}, it will be overwritten during copying. + * + * @param leftVelocity The velocity to set on the left side. + * @param rightVelocity The velocity to set on the right side. + * @return A new {@code TankDriveData} with all fields identical except for the left and right + * {@link DriveData#velocity velocity} fields. + */ + @NotNull + @Contract(value = "_, _ -> new", pure = true) + public TankDriveData withVelocities(double leftVelocity, double rightVelocity) { + return new TankDriveData(left.withVelocity(leftVelocity), right.withVelocity(rightVelocity), + heading, turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} and sets its left and right {@link + * DriveData#acceleration accelerations}. If there is already a value present in either {@link + * DriveData#acceleration acceleration}, it will be overwritten during copying. + * + * @param leftAcceleration The acceleration to set on the left side. + * @param rightAcceleration The acceleration to set on the right side. + * @return A new {@code TankDriveData} with all fields identical except for the left and right + * {@link DriveData#acceleration acceleration} fields. + */ + @NotNull + @Contract(value = "_, _ -> new", pure = true) + public TankDriveData withAccelerations(double leftAcceleration, double rightAcceleration) { + return new TankDriveData(left.withAcceleration(leftAcceleration), + right.withAcceleration(rightAcceleration), heading, turningRate); + } + + /** + * Creates a copy of this {@code TankDriveData} and sets its left and right {@link + * DriveData#additionalFeedForward additionalFeedForwards}. If there is already a value present in + * either {@link DriveData#additionalFeedForward additionalFeedForward}, it will be overwritten + * during copying. + * + * @param leftAdditionalFeedForward The additionalFeedForward to set on the left side. + * @param rightAdditionalFeedForward The additionalFeedForward to set on the right side. + * @return A new {@code TankDriveData} with all fields identical except for the left and right + * {@link DriveData#additionalFeedForward additionalFeedForward} fields. + */ + @NotNull + @Contract(value = "_, _ -> new", pure = true) + public TankDriveData withAdditionalFeedForwards(double leftAdditionalFeedForward, + double rightAdditionalFeedForward) { + return new TankDriveData(left.withAdditionalFeedForward(leftAdditionalFeedForward), + right.withAdditionalFeedForward(rightAdditionalFeedForward), heading, turningRate); + } + /** * Creates a new {@code TankDriveData} with the supplied values. * @@ -65,4 +680,25 @@ public String toString() { + (heading.isPresent() ? ", heading " + heading.getAsDouble() : "") + (turningRate.isPresent() ? ", turning rate " + turningRate.getAsDouble() : ""); } + + @Override + @Contract(value = "null -> false", pure = true) + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TankDriveData)) { + return false; + } + TankDriveData that = (TankDriveData) o; + return left.equals(that.left) && + right.equals(that.right) && + heading.equals(that.heading) && + turningRate.equals(that.turningRate); + } + + @Override + public int hashCode() { + return Objects.hash(left, right, heading, turningRate); + } } diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/TurningRatePIDProcessor.java b/src/main/java/org/team1540/rooster/drive/pipeline/TurningRatePIDProcessor.java new file mode 100644 index 00000000..22c1cadb --- /dev/null +++ b/src/main/java/org/team1540/rooster/drive/pipeline/TurningRatePIDProcessor.java @@ -0,0 +1,117 @@ +package org.team1540.rooster.drive.pipeline; + +import java.util.Objects; +import java.util.function.DoubleSupplier; +import org.jetbrains.annotations.NotNull; +import org.team1540.rooster.functional.Processor; + +/** + * Modifies velocity set-points to keep a desired turning rate. This {@link Processor} accepts a + * {@link TankDriveData} and uses the {@linkplain TankDriveData#turningRate turning rate} as an + * input to the loop. + * + * @see Processor + * @see PIDProcessor + */ +public class TurningRatePIDProcessor extends PIDProcessor { + + private DoubleSupplier yawRateSupplier; + private boolean invertSides; + + @Override + protected double getError(TankDriveData data) { + return data.turningRate.isPresent() ? data.turningRate.getAsDouble() - yawRateSupplier + .getAsDouble() : 0; + } + + @Override + protected TankDriveData createOutput(TankDriveData data, double loopOutput) { + // multiplying the output by -1 effectively flips the sides + loopOutput *= invertSides ? -1 : 1; + + return data.plusVelocities(-loopOutput, loopOutput); + } + + /** + * Creates a new {@code TurningRateClosedLoopProcessor} with P and I D coefficients of 0 that does + * not invert sides. + * + * @param yawRateSupplier A {@link DoubleSupplier} that supplies the current yaw rate in radians + * per second. + * @param p The P coefficient of the PID loop. + */ + public TurningRatePIDProcessor(DoubleSupplier yawRateSupplier, double p) { + this(yawRateSupplier, p, 0); + } + + /** + * Creates a new {@code TurningRateClosedLoopProcessor} with a D coefficient of 0 that does not + * invert sides. + * + * @param yawRateSupplier A {@link DoubleSupplier} that supplies the current yaw rate in radians + * per second. + * @param p The P coefficient of the PID loop. + * @param i The I coefficient of the PID loop. + */ + public TurningRatePIDProcessor(DoubleSupplier yawRateSupplier, double p, double i) { + this(yawRateSupplier, p, i, 0); + } + + /** + * Creates a new {@code TurningRateClosedLoopProcessor} that does not invert sides. + * + * @param yawRateSupplier A {@link DoubleSupplier} that supplies the current yaw rate in radians + * per second. + * @param p The P coefficient of the PID loop. + * @param i The I coefficient of the PID loop. + * @param d The D coefficient of the PID loop. + */ + public TurningRatePIDProcessor(DoubleSupplier yawRateSupplier, double p, double i, + double d) { + this(yawRateSupplier, p, i, d, false); + } + + /** + * Creates a new {@code TurningRateClosedLoopProcessor} with I and D coefficients of 0. + * + * @param yawRateSupplier A {@link DoubleSupplier} that supplies the current yaw rate in radians + * per second. + * @param p The P coefficient of the PID loop. + * @param invertSides Whether to invert the output of the PID loop before sending it onwards. + */ + public TurningRatePIDProcessor(DoubleSupplier yawRateSupplier, double p, + boolean invertSides) { + this(yawRateSupplier, p, 0, invertSides); + } + + /** + * Creates a new {@code TurningRateClosedLoopProcessor} with a D coefficient of 0. + * + * @param yawRateSupplier A {@link DoubleSupplier} that supplies the current yaw rate in radians + * per second. + * @param p The P coefficient of the PID loop. + * @param i The I coefficient of the PID loop. + * @param invertSides Whether to invert the output of the PID loop before sending it onwards. + */ + public TurningRatePIDProcessor(DoubleSupplier yawRateSupplier, double p, double i, + boolean invertSides) { + this(yawRateSupplier, p, i, 0, invertSides); + } + + /** + * Creates a new {@code TurningRateClosedLoopProcessor}. + * + * @param yawRateSupplier A {@link DoubleSupplier} that supplies the current yaw rate in radians + * per second. + * @param p The P coefficient of the PID loop. + * @param i The I coefficient of the PID loop. + * @param d The D coefficient of the PID loop. + * @param invertSides Whether to invert the output of the PID loop before sending it onwards. + */ + public TurningRatePIDProcessor(@NotNull DoubleSupplier yawRateSupplier, double p, double i, + double d, boolean invertSides) { + super(p, i, d); + this.yawRateSupplier = Objects.requireNonNull(yawRateSupplier); + this.invertSides = invertSides; + } +} diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/UnitScaler.java b/src/main/java/org/team1540/rooster/drive/pipeline/UnitScaler.java index f9b10675..32afd06a 100644 --- a/src/main/java/org/team1540/rooster/drive/pipeline/UnitScaler.java +++ b/src/main/java/org/team1540/rooster/drive/pipeline/UnitScaler.java @@ -1,6 +1,7 @@ package org.team1540.rooster.drive.pipeline; import java.util.OptionalDouble; +import org.team1540.rooster.functional.Processor; /** * Scales units by a desired factor. For details of the scaling, see {@link #apply(TankDriveData) diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/package-info.java b/src/main/java/org/team1540/rooster/drive/pipeline/package-info.java index 22280472..3d01b5dc 100644 --- a/src/main/java/org/team1540/rooster/drive/pipeline/package-info.java +++ b/src/main/java/org/team1540/rooster/drive/pipeline/package-info.java @@ -1,9 +1,8 @@ /** * Collection of useful inputs, processors, and outputs. Contains "stock" or "in-the-box" classes - * that implement the {@link org.team1540.rooster.drive.pipeline.Input Input}, {@link - * org.team1540.rooster.drive.pipeline.Processor Processor}, and {@link - * org.team1540.rooster.drive.pipeline.Output Output} interfaces (as well as those interfaces - * themselves). + * that implement the {@link org.team1540.rooster.functional.Input Input}, {@link + * org.team1540.rooster.functional.Processor Processor}, and {@link org.team1540.rooster.functional.Output + * Output} interfaces. * *

    Unless otherwise noted, all methods, constructors, etc. in this package throw a {@link * java.lang.NullPointerException} if any parameters are {@code null}. diff --git a/src/main/java/org/team1540/rooster/functional/Executable.java b/src/main/java/org/team1540/rooster/functional/Executable.java new file mode 100644 index 00000000..01a94db8 --- /dev/null +++ b/src/main/java/org/team1540/rooster/functional/Executable.java @@ -0,0 +1,13 @@ +package org.team1540.rooster.functional; + +/** + * Functional interface for a function that takes no arguments and returns no output. + */ +@FunctionalInterface +public interface Executable { + + /** + * Perform the required action. + */ + void execute(); +} diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/Input.java b/src/main/java/org/team1540/rooster/functional/Input.java similarity index 86% rename from src/main/java/org/team1540/rooster/drive/pipeline/Input.java rename to src/main/java/org/team1540/rooster/functional/Input.java index 24cf0beb..72f9e72b 100644 --- a/src/main/java/org/team1540/rooster/drive/pipeline/Input.java +++ b/src/main/java/org/team1540/rooster/functional/Input.java @@ -1,17 +1,15 @@ -package org.team1540.rooster.drive.pipeline; +package org.team1540.rooster.functional; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import org.team1540.rooster.util.Executable; /** * Extension of a {@link Supplier} adding additional composition methods. * * {@code Input} can be used exactly like {@link Supplier} (and library functions should not take * {@code Outputs} as method parameters, instead using {@link Supplier}). However, library functions - * pertaining to drive pipelines should return an {@code Input} where they would normally return a - * {@link Supplier}. + * should return an {@code Input} where they would normally return a {@link Supplier}. * * @param The type of the input. */ diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/Output.java b/src/main/java/org/team1540/rooster/functional/Output.java similarity index 86% rename from src/main/java/org/team1540/rooster/drive/pipeline/Output.java rename to src/main/java/org/team1540/rooster/functional/Output.java index 0d7dee16..23bab8ee 100644 --- a/src/main/java/org/team1540/rooster/drive/pipeline/Output.java +++ b/src/main/java/org/team1540/rooster/functional/Output.java @@ -1,17 +1,15 @@ -package org.team1540.rooster.drive.pipeline; +package org.team1540.rooster.functional; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import org.team1540.rooster.util.Executable; /** * Extension of a {@link Consumer} adding an additional composition method. * * {@code Output} can be used exactly like {@link Consumer} (and library functions should not take * {@code Outputs} as method parameters, instead using {@link Consumer}). However, library functions - * pertaining to drive pipelines should return an {@code Output} where they would normally return a - * {@link Consumer}. + * should return an {@code Output} where they would normally return a {@link Consumer}. * * @param The type of the output. */ diff --git a/src/main/java/org/team1540/rooster/drive/pipeline/Processor.java b/src/main/java/org/team1540/rooster/functional/Processor.java similarity index 87% rename from src/main/java/org/team1540/rooster/drive/pipeline/Processor.java rename to src/main/java/org/team1540/rooster/functional/Processor.java index e34480fd..d6fb090c 100644 --- a/src/main/java/org/team1540/rooster/drive/pipeline/Processor.java +++ b/src/main/java/org/team1540/rooster/functional/Processor.java @@ -1,4 +1,4 @@ -package org.team1540.rooster.drive.pipeline; +package org.team1540.rooster.functional; import java.util.function.Consumer; import java.util.function.Function; @@ -9,8 +9,7 @@ * * {@code Processor} can be used exactly like {@link Function} (and library functions should not * take {@code Processors} as method parameters, instead using {@link Function}). However, library - * functions pertaining to drive pipelines should return a {@code Processor} where they would - * normally return a {@link Function}. + * functions should return a {@code Processor} where they would normally return a {@link Function}. * * @param The type of the output. */ diff --git a/src/main/java/org/team1540/rooster/functional/package-info.java b/src/main/java/org/team1540/rooster/functional/package-info.java new file mode 100644 index 00000000..f7d341d6 --- /dev/null +++ b/src/main/java/org/team1540/rooster/functional/package-info.java @@ -0,0 +1,10 @@ +/** + * Extensions and additions to java's {@linkplain java.util.function functional interfaces}. The + * extensions of common standard library functional interfaces contained in this package (namely + * {@link org.team1540.rooster.functional.Input}, {@link org.team1540.rooster.functional.Processor}, + * and {@link org.team1540.rooster.functional.Output}) add some default methods to simplify chaining + * (especially in the scenario of constructing a {@linkplain org.team1540.rooster.drive.pipeline + * drive pipeline}). This package also contains the common {@link org.team1540.rooster.functional.Executable} + * functional interface for a function that takes no arguments and returns no result. + */ +package org.team1540.rooster.functional; diff --git a/src/main/java/org/team1540/rooster/motionprofiling/MotionProfile.java b/src/main/java/org/team1540/rooster/motionprofiling/MotionProfile.java new file mode 100644 index 00000000..03b8c148 --- /dev/null +++ b/src/main/java/org/team1540/rooster/motionprofiling/MotionProfile.java @@ -0,0 +1,138 @@ +package org.team1540.rooster.motionprofiling; + +import java.util.Objects; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A sequence of {@link Point Points} that can be used by motion profiling systems. + */ +public class MotionProfile { + + /** + * The {@link Point Points} in the motion profile. + */ + @NotNull + public final Point[] points; + + /** + * Create a new {@link MotionProfile} from an array of points. + * + * @param points The points to use. + */ + public MotionProfile(@NotNull Point[] points) { + this.points = points; + } + + /** + * Gets the nth {@link Point} (0-indexed) in the motion profile. + * + * @param index The index of the point to get. + * @return The point at the specified index. + * @throws ArrayIndexOutOfBoundsException if {@code index} ≥ {@link #size()}. + */ + @Contract(pure = true) + public Point get(int index) { + return points[index]; + } + + /** + * Get the number of {@link Point points} in the profile. + * + * @return The number of points in the profile; specifically, {@code points.length}. + */ + @Contract(pure = true) + public int size() { + return points.length; + } + + /** + * A single instant within a {@link MotionProfile}. + */ + public static class Point { + + /** + * The time change since the previous point, in seconds. + */ + public double dt; + /** + * The x-position of the robot, or 0 if not applicable. + */ + public double x; + /** + * The y-position of the robot, or 0 if not applicable. + */ + public double y; + /** + * The position of the profiled mechanism. + */ + public double position; + /** + * The velocity of the profiled mechanism, in position units per second. + */ + public double velocity; + /** + * The acceleration of the profiled mechanism, in position units per second squared. + */ + public double acceleration; + /** + * The jerk of the profiled mechanism, in position units per second cubed. + */ + public double jerk; + /** + * The robot's heading in radians, or 0 if not applicable. + */ + public double heading; + + /** + * Creates a new {@code Point}. + * + * @param dt The time change since the previous point, in seconds. + * @param x The x-position of the robot, or 0 if not applicable. + * @param y The y-position of the robot, or 0 if not applicable. + * @param position The position of the profiled mechanism. + * @param velocity The velocity of the profiled mechanism, in position units per second. + * @param acceleration The acceleration of the profiled mechanism, in position units per second + * squared. + * @param jerk The jerk of the profiled mechanism, in position units per second cubed. + * @param heading The robot's heading in radians, or 0 if not applicable. + */ + public Point(double dt, double x, double y, double position, double velocity, + double acceleration, double jerk, double heading) { + this.dt = dt; + this.x = x; + this.y = y; + this.position = position; + this.velocity = velocity; + this.acceleration = acceleration; + this.jerk = jerk; + this.heading = heading; + } + + @Contract(value = "null -> false", pure = true) + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Point)) { + return false; + } + Point point = (Point) o; + return Double.compare(point.dt, dt) == 0 && + Double.compare(point.x, x) == 0 && + Double.compare(point.y, y) == 0 && + Double.compare(point.position, position) == 0 && + Double.compare(point.velocity, velocity) == 0 && + Double.compare(point.acceleration, acceleration) == 0 && + Double.compare(point.jerk, jerk) == 0 && + Double.compare(point.heading, heading) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(dt, x, y, position, velocity, acceleration, jerk, heading); + } + } +} diff --git a/src/main/java/org/team1540/rooster/motionprofiling/MotionProfileUtils.java b/src/main/java/org/team1540/rooster/motionprofiling/MotionProfileUtils.java new file mode 100644 index 00000000..fd9e0563 --- /dev/null +++ b/src/main/java/org/team1540/rooster/motionprofiling/MotionProfileUtils.java @@ -0,0 +1,30 @@ +package org.team1540.rooster.motionprofiling; + +import jaci.pathfinder.Trajectory; +import java.util.Arrays; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.team1540.rooster.motionprofiling.MotionProfile.Point; + +public class MotionProfileUtils { + + private MotionProfileUtils() { + } + + /** + * Creates a ROOSTER {@link MotionProfile} from a Pathfinder {@link Trajectory}. + * + * @param trajectory The {@link Trajectory} to convert. + * @return A {@link MotionProfile} containing the same points. Profile points are copied over, so + * subsequent changes to the {@link Trajectory} will not affect the produced {@link + * MotionProfile}. + */ + @Contract("_ -> new") + @NotNull + public static MotionProfile createProfile(@NotNull Trajectory trajectory) { + return new MotionProfile(Arrays.stream(trajectory.segments).map( + s -> new Point(s.dt, s.x, s.y, s.position, s.velocity, s.acceleration, s.jerk, s.heading)) + .toArray(Point[]::new)); + } + +} diff --git a/src/main/java/org/team1540/rooster/motionprofiling/MotionProfilingProperties.java b/src/main/java/org/team1540/rooster/motionprofiling/MotionProfilingProperties.java index deecf25e..6238b7d9 100644 --- a/src/main/java/org/team1540/rooster/motionprofiling/MotionProfilingProperties.java +++ b/src/main/java/org/team1540/rooster/motionprofiling/MotionProfilingProperties.java @@ -4,6 +4,11 @@ import java.util.function.DoubleConsumer; import java.util.function.DoubleSupplier; +/** + * @deprecated Replaced by the {@link org.team1540.rooster.drive.pipeline drive pipeline}-based + * system. + */ +@Deprecated public class MotionProfilingProperties { private double encoderTicksPerUnit = 1023 * 0.1 * Math.PI; diff --git a/src/main/java/org/team1540/rooster/motionprofiling/ProfileContainer.java b/src/main/java/org/team1540/rooster/motionprofiling/ProfileContainer.java new file mode 100644 index 00000000..b0359599 --- /dev/null +++ b/src/main/java/org/team1540/rooster/motionprofiling/ProfileContainer.java @@ -0,0 +1,188 @@ +package org.team1540.rooster.motionprofiling; + +import edu.wpi.first.wpilibj.DriverStation; +import jaci.pathfinder.Pathfinder; +import java.io.File; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Class to preload motion profiles from a specified folder on disk. The preloaded motion profiles + * are stored in RAM so as to be quickly accessible. + * + *

    The folder provided should contain profile CSV files (formatted so as to be read by {@link + * Pathfinder#readFromCSV(File)}) where each profile named {@code name} is stored in two files, + * {@code name}_left.csv for the left-side profile and {@code name}_right.csv for the right-side + * profile. An example folder structure would be as follows (where the profiles/ directory is + * provided to the constructor): + * + *

    + * profiles/
    + *   foo_left.csv
    + *   foo_right.csv
    + *   bar_left.csv
    + *   bar_right.csv
    + * 
    + * + * This would cause the {@code ProfileContainer} to load two profiles named {@code foo} and {@code + * bar}. + * + *

    This class is immutable after instantiation. + */ +public class ProfileContainer { + + @NotNull + private Map profiles; + + /** + * Creates a new {@code ProfileContainer} that searches the provided directory using a left suffix + * of "{@code _left.csv}" and a right suffix of "{@code _right.csv}" + * + * This constructor also searches the provided directory for profiles and loads all the profiles + * into RAM. For this reason, initialization may take some time (especially for large amounts of + * profiles). + * + * @param profileDirectory The directory containing the profiles. See the {@linkplain + * ProfileContainer class documentation} for a description of the folder structure. + * @throws RuntimeException If an I/O error occurs during profile loading. + */ + public ProfileContainer(@NotNull File profileDirectory) { + this(profileDirectory, "_left.csv", "_right.csv"); + } + + /** + * Creates a new {@code ProfileContainer}. This constructor also searches the provided directory + * for profiles and loads all the profiles into RAM. For this reason, initialization may take some + * time (especially for large amounts of profiles). + * + * @param profileDirectory The directory containing the profiles. See the {@linkplain + * ProfileContainer class documentation} for a description of the folder structure. + * @param leftSuffix The suffix to use to identify left-side profile files. + * @param rightSuffix The suffix to use to identify right-side profile files. + * @throws RuntimeException If an I/O error occurs during profile loading. + */ + public ProfileContainer(@NotNull File profileDirectory, @NotNull String leftSuffix, + @NotNull String rightSuffix) { + if (!profileDirectory.isDirectory()) { + throw new IllegalArgumentException("Not a directory"); + } + + File[] lFiles = profileDirectory.listFiles((file) -> file.getName().endsWith(leftSuffix)); + File[] rFiles = profileDirectory.listFiles((file) -> file.getName().endsWith(rightSuffix)); + + if (lFiles == null || rFiles == null) { + // according to listFiles() docs, it will only return null if the file isn't a directory + // (which we've already checked) or if an IO error occurs. Thus, if lFiles or rFiles is + // null we know an IO error happened. Not throwing an IOException because checked exceptions + // are bad, in constructors even more so. + throw new RuntimeException("IO error occurred while reading files"); + } + + Set profileNames = new HashSet<>(); + + for (File f : lFiles) { + profileNames.add(f.getName().substring(0, f.getName().length() - leftSuffix.length())); + } + + for (File f : rFiles) { + profileNames.add(f.getName().substring(0, f.getName().length() - rightSuffix.length())); + } + + // initialize the map once we know the number of profiles so it doesn't expand. + // Why? p e r f o r m a n c e + + profiles = new HashMap<>(profileNames.size()); + + for (String name : profileNames) { + System.out.println("Loading profile " + name); + File leftFile = Arrays.stream(lFiles) + .filter(file -> file.getName().equals(name + leftSuffix)) + .findFirst() + .orElseGet(() -> { + DriverStation + .reportWarning("Left-side file for profile " + name + " does not exist", false); + return null; + }); + File rightFile = Arrays.stream(rFiles) + .filter(file -> file.getName().equals(name + rightSuffix)) + .findFirst() + .orElseGet(() -> { + DriverStation + .reportWarning("Right-side file for profile " + name + " does not exist", false); + return null; + }); + + if (leftFile != null && rightFile != null) { + MotionProfile left = MotionProfileUtils.createProfile(Pathfinder.readFromCSV(leftFile)); + MotionProfile right = MotionProfileUtils.createProfile(Pathfinder.readFromCSV(rightFile)); + + profiles.put(name, new DriveProfile(left, right)); + } + } + } + + /** + * Returns the motion profile set with the specified name, or {@code null} if the profile does not + * exist. + * + * @param name The name of the profile. + * @return A {@link DriveProfile} containing the left and right profiles, or {@code null} if the + * profile does not exist. + */ + @Nullable + public DriveProfile get(String name) { + return profiles.get(name); + } + + /** + * Get the {@link Set} of profile names in the {@code ProfileContainer}. + * + * @return A {@link Set} containing the names of each profile. + */ + @NotNull + public Set getProfileNames() { + return profiles.keySet(); + } + + /** + * Returns whether or not the {@code ProfileContainer} contains a specific profile. + * + * @param name The name of the profile to check. + * @return {@code true} if the profile exists (i.e. if {@link #get(String) get(name)} would not + * return {@code null}), {@code false} otherwise. + */ + public boolean hasProfile(String name) { + return profiles.containsKey(name); + } + + /** + * Data class for holding left and right trajectories in one object. + */ + public static class DriveProfile { + + @NotNull + private final MotionProfile left; + @NotNull + private final MotionProfile right; + + private DriveProfile(@NotNull MotionProfile left, @NotNull MotionProfile right) { + this.left = left; + this.right = right; + } + + @NotNull + public MotionProfile getLeft() { + return left; + } + + @NotNull + public MotionProfile getRight() { + return right; + } + } +} diff --git a/src/main/java/org/team1540/rooster/motionprofiling/RunMotionProfiles.java b/src/main/java/org/team1540/rooster/motionprofiling/RunMotionProfiles.java index f53a4c26..3bd922db 100644 --- a/src/main/java/org/team1540/rooster/motionprofiling/RunMotionProfiles.java +++ b/src/main/java/org/team1540/rooster/motionprofiling/RunMotionProfiles.java @@ -9,7 +9,11 @@ /** * Executes a set of motion profiles (with respective properties.) + * + * @deprecated Replaced by the {@link org.team1540.rooster.drive.pipeline drive pipeline}-based + * system. */ +@Deprecated public class RunMotionProfiles extends Command { private int slotId = 0; diff --git a/src/main/java/org/team1540/rooster/motionprofiling/package-info.java b/src/main/java/org/team1540/rooster/motionprofiling/package-info.java new file mode 100644 index 00000000..cc7256cf --- /dev/null +++ b/src/main/java/org/team1540/rooster/motionprofiling/package-info.java @@ -0,0 +1,8 @@ +/** + * Utilities for motion profiles. + * + * This class contains common data classes to encapsulate motion profiles as well as a utility + * ({@link org.team1540.rooster.motionprofiling.ProfileContainer}) to load them from disk. It also + * contains legacy profile execution code, kept for backwards compatibility. + */ +package org.team1540.rooster.motionprofiling; diff --git a/src/main/java/org/team1540/rooster/util/DashboardUtils.java b/src/main/java/org/team1540/rooster/util/DashboardUtils.java index 42b9563e..fdb67d0a 100644 --- a/src/main/java/org/team1540/rooster/util/DashboardUtils.java +++ b/src/main/java/org/team1540/rooster/util/DashboardUtils.java @@ -6,6 +6,7 @@ import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; import java.util.Objects; import org.jetbrains.annotations.NotNull; +import org.team1540.rooster.functional.Executable; import org.team1540.rooster.preferencemanager.PreferenceManager; /** diff --git a/src/main/java/org/team1540/rooster/util/Executable.java b/src/main/java/org/team1540/rooster/util/Executable.java index a3ff12af..e69de29b 100644 --- a/src/main/java/org/team1540/rooster/util/Executable.java +++ b/src/main/java/org/team1540/rooster/util/Executable.java @@ -1,13 +0,0 @@ -package org.team1540.rooster.util; - -/** - * Interface for actions that can be passed to a {@link SimpleCommand}. - */ -@FunctionalInterface -public interface Executable { - - /** - * Perform the required action. - */ - void execute(); -} diff --git a/src/main/java/org/team1540/rooster/util/SimpleAsyncCommand.java b/src/main/java/org/team1540/rooster/util/SimpleAsyncCommand.java index 93fef841..8412021d 100644 --- a/src/main/java/org/team1540/rooster/util/SimpleAsyncCommand.java +++ b/src/main/java/org/team1540/rooster/util/SimpleAsyncCommand.java @@ -6,6 +6,7 @@ import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; import java.util.Objects; import org.jetbrains.annotations.NotNull; +import org.team1540.rooster.functional.Executable; /** * A simple way to construct an {@link AsyncCommand}.

    To create a {@code SimpleCommand} easily, diff --git a/src/main/java/org/team1540/rooster/util/SimpleCommand.java b/src/main/java/org/team1540/rooster/util/SimpleCommand.java index b9ce154a..af6981ac 100644 --- a/src/main/java/org/team1540/rooster/util/SimpleCommand.java +++ b/src/main/java/org/team1540/rooster/util/SimpleCommand.java @@ -6,6 +6,7 @@ import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; import java.util.Objects; import org.jetbrains.annotations.NotNull; +import org.team1540.rooster.functional.Executable; /** * A simple way to construct an {@link InstantCommand}.

    To create a {@code SimpleCommand} diff --git a/src/main/java/org/team1540/rooster/util/SimpleLoopCommand.java b/src/main/java/org/team1540/rooster/util/SimpleLoopCommand.java index 5f3f4e81..a236015e 100644 --- a/src/main/java/org/team1540/rooster/util/SimpleLoopCommand.java +++ b/src/main/java/org/team1540/rooster/util/SimpleLoopCommand.java @@ -6,6 +6,7 @@ import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; import java.util.Objects; import org.jetbrains.annotations.NotNull; +import org.team1540.rooster.functional.Executable; /** * A simple way to construct a {@link Command} which executes every tick.

    To create a {@code diff --git a/src/main/java/org/team1540/rooster/util/robots/AccelerationCharacterizationRobot.java b/src/main/java/org/team1540/rooster/util/robots/AccelerationCharacterizationRobot.java new file mode 100644 index 00000000..7e032da0 --- /dev/null +++ b/src/main/java/org/team1540/rooster/util/robots/AccelerationCharacterizationRobot.java @@ -0,0 +1,233 @@ +package org.team1540.rooster.util.robots; + +import com.ctre.phoenix.motorcontrol.ControlMode; +import com.ctre.phoenix.motorcontrol.FeedbackDevice; +import edu.wpi.first.wpilibj.IterativeRobot; +import edu.wpi.first.wpilibj.Joystick; +import edu.wpi.first.wpilibj.Notifier; +import edu.wpi.first.wpilibj.command.Scheduler; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.util.LinkedList; +import java.util.List; +import org.apache.commons.math3.stat.regression.SimpleRegression; +import org.team1540.rooster.preferencemanager.Preference; +import org.team1540.rooster.preferencemanager.PreferenceManager; +import org.team1540.rooster.wrappers.ChickenTalon; + +/** + * Self-contained robot class to characterize a drivetrain's acceleration term. + * + * When deployed to a three-motor-per-side robot with left motors on motors 1, 2, and 3 and right + * motors on motors 4, 5, and 6, whenever the A button is pressed and held during teleop the robot + * will carry out an acceleration characterization as described in Eli Barnett's paper "FRC + * Drivetrain Characterization" until the button is released. (Encoders should be on motors 1 and + * 4.) The results will be saved in a file in the /home/lvuser/dtmeasure directory named + * "measureaccel-" followed by the Unix timestamp of the test start and ".csv". + */ +public class AccelerationCharacterizationRobot extends IterativeRobot { + + @Preference("kV") + public double kV; + @Preference("VIntercept") + public double vIntercept; + + @Preference("Invert Left Motor") + public boolean invertLeft = false; + @Preference("Invert Right Motor") + public boolean invertRight = true; + @Preference(persistent = false) + public int accelMeasurementWindow = 6; + + @Preference(persistent = false) + public double setpoint = 0.6; + + private ChickenTalon driveLeftMotorA = new ChickenTalon(1); + private ChickenTalon driveLeftMotorB = new ChickenTalon(2); + private ChickenTalon driveLeftMotorC = new ChickenTalon(3); + private ChickenTalon[] driveLeftMotors = new ChickenTalon[]{driveLeftMotorA, driveLeftMotorB, + driveLeftMotorC}; + private ChickenTalon driveRightMotorA = new ChickenTalon(4); + private ChickenTalon driveRightMotorB = new ChickenTalon(5); + private ChickenTalon driveRightMotorC = new ChickenTalon(6); + private ChickenTalon[] driveRightMotors = new ChickenTalon[]{driveRightMotorA, driveRightMotorB, + driveRightMotorC}; + private ChickenTalon[] driveMotorAll = new ChickenTalon[]{driveLeftMotorA, driveLeftMotorB, + driveLeftMotorC, driveRightMotorA, driveRightMotorB, driveRightMotorC}; + private ChickenTalon[] driveMotorMasters = new ChickenTalon[]{driveLeftMotorA, driveRightMotorA}; + + private PrintWriter csvWriter = null; + + private Notifier notifier = new Notifier(this::run); + + @SuppressWarnings("SuspiciousNameCombination") + private void run() { + if (!isOperatorControl() && csvWriter != null) { + csvWriter.close(); + csvWriter = null; + } + if (leftVelocities.size() == accelMeasurementWindow) { + leftVelocities.remove(0); + rightVelocities.remove(0); + leftVoltages.remove(0); + rightVoltages.remove(0); + times.remove(0); + } + double leftVelocity = driveLeftMotorA.getSelectedSensorVelocity(); + double rightVelocity = driveRightMotorA.getSelectedSensorVelocity(); + + leftVelocities.add(leftVelocity); + rightVelocities.add(rightVelocity); + + double accelCausingVoltageLeft = + driveLeftMotorA.getMotorOutputVoltage() - (kV * leftVelocity + + vIntercept); + double accelCausingVoltageRight = + driveRightMotorA.getMotorOutputVoltage() - (kV * rightVelocity + + vIntercept); + leftVoltages.add(accelCausingVoltageLeft); + rightVoltages.add(accelCausingVoltageRight); + times.add((double) System.currentTimeMillis() / 1000.0); + + if (leftVelocities.size() == accelMeasurementWindow) { + double lAccel = bestFitSlope(times, leftVelocities); + double rAccel = bestFitSlope(times, rightVelocities); + SmartDashboard.putNumber("Left Accel", lAccel); + SmartDashboard.putNumber("Right Accel", rAccel); + } + + if (joystick.getRawButton(1)) { // if button A is pressed + if (csvWriter == null) { + // create a new CSV writer, reset everything + reset(); + try { + File dir = new File("/home/lvuser/dtmeasure/"); + if (!dir.exists()) { + dir.mkdirs(); + } + csvWriter = new PrintWriter(new File( + "/home/lvuser/dtmeasure/measureaccel-" + System.currentTimeMillis() + ".csv")); + csvWriter.println("lvoltage,laccel,rvoltage,raccel"); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + driveLeftMotorA.set(ControlMode.PercentOutput, 0); + driveRightMotorA.set(ControlMode.PercentOutput, 0); + } else { + SmartDashboard.putNumber("Left Output", driveLeftMotorA.getMotorOutputPercent()); + SmartDashboard.putNumber("Left Velocity", leftVelocity); + SmartDashboard.putNumber("Right Output", driveRightMotorA.getMotorOutputPercent()); + SmartDashboard.putNumber("Right Velocity", rightVelocity); + + if (leftVelocities.size() == accelMeasurementWindow) { + double lAccel = bestFitSlope(times, leftVelocities); + double rAccel = bestFitSlope(times, rightVelocities); + csvWriter.println( + leftVoltages.get(1) + "," + lAccel + "," + rightVoltages.get(1) + "," + rAccel); + System.out.println(leftVelocities.toString()); + System.out.println(times.toString()); + System.out.println(lAccel); + } + driveLeftMotorA.set(ControlMode.PercentOutput, setpoint); + driveRightMotorA.set(ControlMode.PercentOutput, setpoint); + } + } else { + if (csvWriter != null) { + csvWriter.close(); + csvWriter = null; + } + driveLeftMotorA.set(ControlMode.PercentOutput, 0); + driveRightMotorA.set(ControlMode.PercentOutput, 0); + } + } + + private Joystick joystick = new Joystick(0); + + private List leftVelocities = new LinkedList<>(); + private List leftVoltages = new LinkedList<>(); + private List rightVelocities = new LinkedList<>(); + private List rightVoltages = new LinkedList<>(); + private List times = new LinkedList<>(); + + @Override + public void robotInit() { + PreferenceManager.getInstance().add(this); + reset(); + notifier.startPeriodic(0.01); + + SmartDashboard.setDefaultNumber("Setpoint", 0.6); + } + + @Override + public void robotPeriodic() { + Scheduler.getInstance().run(); // process preferences + } + + @Override + public void teleopPeriodic() { + + } + + @Override + public void teleopInit() { + reset(); + for (ChickenTalon talon : driveMotorAll) { + talon.setBrake(true); + } + } + + @Override + public void disabledInit() { + for (ChickenTalon talon : driveMotorAll) { + talon.setBrake(false); + } + } + + @SuppressWarnings("Duplicates") + public void reset() { + driveLeftMotorA.configSelectedFeedbackSensor(FeedbackDevice.QuadEncoder); + driveRightMotorA.configSelectedFeedbackSensor(FeedbackDevice.QuadEncoder); + + driveLeftMotorA.setSensorPhase(true); + + for (ChickenTalon talon : driveLeftMotors) { + talon.setInverted(invertLeft); + } + + driveRightMotorA.setSensorPhase(true); + + for (ChickenTalon talon : driveRightMotors) { + talon.setInverted(invertRight); + } + + driveLeftMotorB.set(ControlMode.Follower, driveLeftMotorA.getDeviceID()); + driveLeftMotorC.set(ControlMode.Follower, driveLeftMotorA.getDeviceID()); + + driveRightMotorB.set(ControlMode.Follower, driveRightMotorA.getDeviceID()); + driveRightMotorC.set(ControlMode.Follower, driveRightMotorA.getDeviceID()); + + for (ChickenTalon talon : driveMotorAll) { + talon.setBrake(true); + } + + for (ChickenTalon talon : driveMotorAll) { + talon.configClosedloopRamp(0); + talon.configOpenloopRamp(0); + talon.configPeakOutputForward(1); + talon.configPeakOutputReverse(-1); + talon.enableCurrentLimit(false); + talon.configVoltageCompSaturation(12); + talon.enableVoltageCompensation(true); + } + } + + private static double bestFitSlope(List xVals, List yVals) { + SimpleRegression reg = new SimpleRegression(); + for (int i = 0; i < xVals.size(); i++) { + reg.addData(xVals.get(i), yVals.get(i)); + } + return reg.getSlope(); + } +} diff --git a/src/main/java/org/team1540/rooster/util/robots/VelocityCharacterizationRobot.java b/src/main/java/org/team1540/rooster/util/robots/VelocityCharacterizationRobot.java new file mode 100644 index 00000000..3ed95fdc --- /dev/null +++ b/src/main/java/org/team1540/rooster/util/robots/VelocityCharacterizationRobot.java @@ -0,0 +1,240 @@ +package org.team1540.rooster.util.robots; + +import com.ctre.phoenix.motorcontrol.ControlMode; +import edu.wpi.first.wpilibj.IterativeRobot; +import edu.wpi.first.wpilibj.Joystick; +import edu.wpi.first.wpilibj.command.Command; +import edu.wpi.first.wpilibj.command.Scheduler; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import org.apache.commons.math3.stat.regression.SimpleRegression; +import org.jetbrains.annotations.NotNull; +import org.team1540.rooster.preferencemanager.Preference; +import org.team1540.rooster.preferencemanager.PreferenceManager; +import org.team1540.rooster.util.SimpleCommand; +import org.team1540.rooster.wrappers.ChickenTalon; + +//TODO MORE DOCS + +/** + * Self-contained robot class to characterize a drivetrain's velocity term. + * + * Whenever the A button is pressed and held during teleop the robot will carry out a quasi-static + * velocity characterization as described in Eli Barnett's paper "FRC Drivetrain Characterization" + * until the button is released. The results (kV, vIntercept, and an R^2 value) are output to the + * SmartDashboard. + */ +public class VelocityCharacterizationRobot extends IterativeRobot { + + @Preference(persistent = false) + public int lMotor1ID = -1; + @Preference(persistent = false) + public int lMotor2ID = -1; + @Preference(persistent = false) + public int lMotor3ID = -1; + @Preference(persistent = false) + public int rMotor1ID = -1; + @Preference(persistent = false) + public int rMotor2ID = -1; + @Preference(persistent = false) + public int rMotor3ID = -1; + @Preference(persistent = false) + public boolean invertLeftMotor = false; + @Preference(persistent = false) + public boolean invertRightMotor = false; + @Preference(persistent = false) + public boolean invertLeftSensor = false; + @Preference(persistent = false) + public boolean invertRightSensor = false; + @Preference(persistent = false) + public boolean brake = false; + @Preference(persistent = false) + public double tpu = 1; + + + private ChickenTalon driveLeftMotorA; + private ChickenTalon driveLeftMotorB; + private ChickenTalon driveLeftMotorC; + private ChickenTalon driveRightMotorA; + private ChickenTalon driveRightMotorB; + private ChickenTalon driveRightMotorC; + + private SimpleRegression leftRegression = new SimpleRegression(); + private SimpleRegression rightRegression = new SimpleRegression(); + private boolean running = false; + + private Joystick joystick = new Joystick(0); + + private static final double SATURATION_VOLTAGE = 12; + @Preference("Voltage Ramp Rate (V/sec)") + public double rampRate = 0.25; + + @Preference("Invert Left Motor") + public boolean invertLeft = false; + @Preference("Invert Right Motor") + public boolean invertRight = true; + + private double appliedOutput = 0; + + public long lastTime; + + @Override + public void robotInit() { + PreferenceManager.getInstance().add(this); + Command reset = new SimpleCommand("Reset", () -> { + if (lMotor1ID != -1) { + driveLeftMotorA = new ChickenTalon(lMotor1ID); + } else { + System.err.println("Left Motor 1 must be set!"); + return; + } + if (lMotor2ID != -1) { + driveLeftMotorB = new ChickenTalon(lMotor2ID); + driveLeftMotorB.set(ControlMode.Follower, driveLeftMotorA.getDeviceID()); + } else { + if (driveLeftMotorB != null) { + driveLeftMotorB.set(ControlMode.PercentOutput, 0); + } + driveLeftMotorB = null; + } + if (lMotor3ID != -1) { + driveLeftMotorC = new ChickenTalon(lMotor3ID); + driveLeftMotorC.set(ControlMode.Follower, driveLeftMotorA.getDeviceID()); + } else { + if (driveLeftMotorC != null) { + driveLeftMotorC.set(ControlMode.PercentOutput, 0); + } + driveLeftMotorC = null; + } + + if (rMotor1ID != -1) { + driveRightMotorA = new ChickenTalon(rMotor1ID); + } else { + System.err.println("Right Motor 1 must be set!"); + return; + } + if (rMotor2ID != -1) { + driveRightMotorB = new ChickenTalon(rMotor2ID); + driveRightMotorB.set(ControlMode.Follower, driveRightMotorA.getDeviceID()); + } else { + if (driveRightMotorB != null) { + driveRightMotorB.set(ControlMode.PercentOutput, 0); + } + driveRightMotorB = null; + } + if (rMotor3ID != -1) { + driveRightMotorC = new ChickenTalon(rMotor3ID); + driveRightMotorC.set(ControlMode.Follower, driveRightMotorA.getDeviceID()); + } else { + if (driveRightMotorC != null) { + driveRightMotorC.set(ControlMode.PercentOutput, 0); + } + driveRightMotorC = null; + } + for (ChickenTalon motor : new ChickenTalon[]{driveLeftMotorA, driveLeftMotorB, + driveLeftMotorC, driveRightMotorA, driveRightMotorB, + driveRightMotorC}) { + if (motor != null) { + motor.configVoltageCompSaturation(SATURATION_VOLTAGE); + motor.enableVoltageCompensation(true); + motor.configClosedloopRamp(0); + motor.configOpenloopRamp(0); + motor.configPeakOutputForward(1); + motor.configPeakOutputReverse(-1); + motor.enableCurrentLimit(false); + motor.setBrake(brake); + } + } + }); + reset.setRunWhenDisabled(true); + reset.start(); + SmartDashboard.putData(reset); + + Command zero = new SimpleCommand("Zero", () -> { + if (driveLeftMotorA != null) { + driveLeftMotorA.setSelectedSensorPosition(0); + } + + if (driveRightMotorA != null) { + driveRightMotorA.setSelectedSensorPosition(0); + } + }); + zero.setRunWhenDisabled(true); + SmartDashboard.putData(zero); + } + + private static void putRegressionData(@NotNull SimpleRegression regression, String prefix) { + // getSlope, getIntercept, and getRSquare all have the same criteria for returning NaN + if (!Double.isNaN(regression.getSlope())) { + SmartDashboard.putNumber(prefix + " Calculated kV", regression.getSlope()); + SmartDashboard.putNumber(prefix + " Calculated vIntercept", regression.getIntercept()); + SmartDashboard.putNumber(prefix + " rSquared", regression.getRSquare()); + } else { + SmartDashboard.putNumber(prefix + " Calculated kV", 0); + SmartDashboard.putNumber(prefix + " Calculated vIntercept", 0); + SmartDashboard.putNumber(prefix + " rSquared", 0); + } + } + + @Override + public void robotPeriodic() { + putRegressionData(leftRegression, "Left"); + putRegressionData(rightRegression, "Right"); + Scheduler.getInstance().run(); + if (driveLeftMotorA != null) { + driveLeftMotorA.setSensorPhase(invertLeftSensor); + } + for (ChickenTalon talon : new ChickenTalon[]{driveLeftMotorA, driveLeftMotorB, + driveLeftMotorC}) { + if (talon != null) { + talon.setInverted(invertLeftMotor); + } + } + + if (driveRightMotorA != null) { + driveRightMotorA.setSensorPhase(invertRightSensor); + } + for (ChickenTalon talon : new ChickenTalon[]{driveRightMotorA, driveRightMotorB, + driveRightMotorC}) { + if (talon != null) { + talon.setInverted(invertRightMotor); + } + } + } + + @Override + public void teleopPeriodic() { + if (joystick.getRawButton(1)) { // if button A is pressed + if (!running) { + // reset everything + running = true; + leftRegression.clear(); + rightRegression.clear(); + appliedOutput = 0; + driveLeftMotorA.set(ControlMode.PercentOutput, 0); + driveRightMotorA.set(ControlMode.PercentOutput, 0); + } else { + double leftVelocity = (driveLeftMotorA.getSelectedSensorVelocity() * 10) / tpu; + if (leftVelocity != 0) { + leftRegression.addData(leftVelocity, driveLeftMotorA.getMotorOutputVoltage()); + } + + double rightVelocity = (driveRightMotorA.getSelectedSensorVelocity() * 10) / tpu; + if (rightVelocity != 0) { + rightRegression.addData(leftVelocity, driveRightMotorA.getMotorOutputVoltage()); + } + + appliedOutput += + (rampRate / SATURATION_VOLTAGE) * ((System.currentTimeMillis() - lastTime) / 1000.0); + lastTime = System.currentTimeMillis(); + driveLeftMotorA.set(ControlMode.PercentOutput, appliedOutput); + driveRightMotorA.set(ControlMode.PercentOutput, appliedOutput); + } + } else { + appliedOutput = 0; + driveLeftMotorA.set(ControlMode.PercentOutput, 0); + driveRightMotorA.set(ControlMode.PercentOutput, 0); + running = false; + } + lastTime = System.currentTimeMillis(); + } +} diff --git a/src/main/java/org/team1540/rooster/util/robots/WheelbaseTestRobot.java b/src/main/java/org/team1540/rooster/util/robots/WheelbaseTestRobot.java new file mode 100644 index 00000000..484d7bec --- /dev/null +++ b/src/main/java/org/team1540/rooster/util/robots/WheelbaseTestRobot.java @@ -0,0 +1,189 @@ +package org.team1540.rooster.util.robots; + +import com.ctre.phoenix.motorcontrol.ControlMode; +import edu.wpi.first.wpilibj.IterativeRobot; +import edu.wpi.first.wpilibj.Joystick; +import edu.wpi.first.wpilibj.command.Command; +import edu.wpi.first.wpilibj.command.Scheduler; +import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard; +import org.team1540.rooster.preferencemanager.Preference; +import org.team1540.rooster.preferencemanager.PreferenceManager; +import org.team1540.rooster.util.SimpleCommand; +import org.team1540.rooster.wrappers.ChickenTalon; + +/** + * Class to determine a robot's wheelbase width. For use instructions, load onto a robot and check + * the console. + */ +public class WheelbaseTestRobot extends IterativeRobot { + + @Preference(persistent = false) + public boolean logDataToCSV = false; + @Preference(persistent = false) + public int lMotor1ID = -1; + @Preference(persistent = false) + public int lMotor2ID = -1; + @Preference(persistent = false) + public int lMotor3ID = -1; + @Preference(persistent = false) + public int rMotor1ID = -1; + @Preference(persistent = false) + public int rMotor2ID = -1; + @Preference(persistent = false) + public int rMotor3ID = -1; + @Preference(persistent = false) + public boolean invertLeftMotor = false; + @Preference(persistent = false) + public boolean invertRightMotor = false; + @Preference(persistent = false) + public boolean invertLeftSensor = false; + @Preference(persistent = false) + public boolean invertRightSensor = false; + @Preference(persistent = false) + public boolean brake = false; + @Preference(persistent = false) + public double encoderTPU = 1; + @Preference(persistent = false) + public double setpoint = 0.5; + + private ChickenTalon lMotor1; + private ChickenTalon lMotor2; + private ChickenTalon lMotor3; + private ChickenTalon rMotor1; + private ChickenTalon rMotor2; + private ChickenTalon rMotor3; + + private Joystick joystick = new Joystick(0); + + @Override + public void robotInit() { + System.out.println("Initializing Wheelbase Test Robot"); + System.out.println( + "To change the motors to be used, change the preference values and then run the Reset command to " + + "allow the values to take effect. To disable a motor, set its motor ID to -1. Motor 1 will be " + + "configured as the master Talon and motors 2, 3, and 4 will be slaved to it in follower mode."); + + PreferenceManager.getInstance().add(this); + + Command reset = new SimpleCommand("Reset", () -> { + if (lMotor1ID != -1) { + lMotor1 = new ChickenTalon(lMotor1ID); + lMotor1.setSensorPhase(invertLeftSensor); + } else { + System.err.println("Left Motor 1 must be set!"); + return; + } + if (lMotor2ID != -1) { + lMotor2 = new ChickenTalon(lMotor2ID); + lMotor2.set(ControlMode.Follower, lMotor1.getDeviceID()); + } else { + if (lMotor2 != null) { + lMotor2.set(ControlMode.PercentOutput, 0); + } + lMotor2 = null; + } + if (lMotor3ID != -1) { + lMotor3 = new ChickenTalon(lMotor3ID); + lMotor3.set(ControlMode.Follower, lMotor1.getDeviceID()); + } else { + if (lMotor3 != null) { + lMotor3.set(ControlMode.PercentOutput, 0); + } + lMotor3 = null; + } + + if (rMotor1ID != -1) { + rMotor1 = new ChickenTalon(rMotor1ID); + rMotor1.setSensorPhase(invertRightSensor); + } else { + System.err.println("Right Motor 1 must be set!"); + return; + } + if (rMotor2ID != -1) { + rMotor2 = new ChickenTalon(rMotor2ID); + rMotor2.set(ControlMode.Follower, rMotor1.getDeviceID()); + } else { + if (rMotor2 != null) { + rMotor2.set(ControlMode.PercentOutput, 0); + } + rMotor2 = null; + } + if (rMotor3ID != -1) { + rMotor3 = new ChickenTalon(rMotor3ID); + rMotor3.set(ControlMode.Follower, rMotor1.getDeviceID()); + } else { + if (rMotor3 != null) { + rMotor3.set(ControlMode.PercentOutput, 0); + } + rMotor3 = null; + } + for (ChickenTalon motor : new ChickenTalon[]{lMotor1, lMotor2, lMotor3, rMotor1, rMotor2, + rMotor3}) { + if (motor != null) { + motor.configClosedloopRamp(0); + motor.configOpenloopRamp(0); + motor.configPeakOutputForward(1); + motor.configPeakOutputReverse(-1); + motor.enableCurrentLimit(false); + motor.setBrake(brake); + } + } + + for (ChickenTalon motor : new ChickenTalon[]{lMotor1, lMotor2, lMotor3}) { + motor.setInverted(invertLeftMotor); + } + for (ChickenTalon motor : new ChickenTalon[]{rMotor1, rMotor2, rMotor3}) { + motor.setInverted(invertRightMotor); + } + }); + reset.setRunWhenDisabled(true); + reset.start(); + SmartDashboard.putData(reset); + + Command zero = new SimpleCommand("Zero", () -> { + if (lMotor1 != null) { + lMotor1.setSelectedSensorPosition(0); + } + + if (rMotor1 != null) { + rMotor1.setSelectedSensorPosition(0); + } + }); + zero.setRunWhenDisabled(true); + SmartDashboard.putData(zero); + } + + @Override + public void teleopInit() { + System.out.println("Zero encoders, then press A until the robot completes 10 revolutions"); + } + + @Override + public void teleopPeriodic() { + if (joystick.getRawButton(1)) { // button A + lMotor1.set(ControlMode.PercentOutput, setpoint); + rMotor1.set(ControlMode.PercentOutput, -setpoint); + } else { + lMotor1.set(ControlMode.PercentOutput, 0); + rMotor1.set(ControlMode.PercentOutput, 0); + } + } + + @Override + public void robotPeriodic() { + Scheduler.getInstance().run(); + if (lMotor1 != null && rMotor1 != null) { + SmartDashboard.putNumber("LPOS", lMotor1.getSelectedSensorPosition()); + SmartDashboard.putNumber("RPOS", rMotor1.getSelectedSensorPosition()); + + double leftDistance = lMotor1.getSelectedSensorPosition(); + double rightDistance = -rMotor1.getSelectedSensorPosition(); + + SmartDashboard.putNumber("Left Distance", leftDistance); + SmartDashboard.putNumber("Right Distance", rightDistance); + + SmartDashboard.putNumber("Calculated width (assuming 10 rots)", + (((leftDistance + rightDistance) / 2) / (10 * Math.PI)) / encoderTPU); + } + } +} diff --git a/src/main/kotlin/org/team1540/rooster/drive/pipeline/PipelineExtensions.kt b/src/main/kotlin/org/team1540/rooster/drive/pipeline/PipelineExtensions.kt index 4c27ac01..f1cd7bd0 100644 --- a/src/main/kotlin/org/team1540/rooster/drive/pipeline/PipelineExtensions.kt +++ b/src/main/kotlin/org/team1540/rooster/drive/pipeline/PipelineExtensions.kt @@ -2,7 +2,7 @@ package org.team1540.rooster.drive.pipeline -import org.team1540.rooster.util.Executable +import org.team1540.rooster.functional.Executable import java.util.function.Consumer import java.util.function.Supplier diff --git a/src/testbots/kotlin/org/team1540/rooster/testing/DrivePipelineTesting.kt b/src/testbots/kotlin/org/team1540/rooster/testing/DrivePipelineTesting.kt index 30ce4886..b778c2eb 100644 --- a/src/testbots/kotlin/org/team1540/rooster/testing/DrivePipelineTesting.kt +++ b/src/testbots/kotlin/org/team1540/rooster/testing/DrivePipelineTesting.kt @@ -8,13 +8,18 @@ import edu.wpi.first.wpilibj.command.Scheduler import edu.wpi.first.wpilibj.smartdashboard.SmartDashboard import org.team1540.rooster.Utilities import org.team1540.rooster.drive.pipeline.* +import org.team1540.rooster.functional.Executable +import org.team1540.rooster.functional.Input +import org.team1540.rooster.functional.Processor +import org.team1540.rooster.motionprofiling.ProfileContainer import org.team1540.rooster.preferencemanager.Preference import org.team1540.rooster.preferencemanager.PreferenceManager -import org.team1540.rooster.util.Executable import org.team1540.rooster.util.SimpleAsyncCommand import org.team1540.rooster.util.SimpleCommand import org.team1540.rooster.util.SimpleLoopCommand import org.team1540.rooster.wrappers.ChickenTalon +import java.io.File +import java.util.OptionalDouble import java.util.function.DoubleSupplier /** @@ -82,24 +87,24 @@ class AdvancedJoystickInputPipelineTestRobot : DrivePipelineTestRobot() { override fun robotInit() { PreferenceManager.getInstance().add(this) val reset = SimpleCommand("reset", Executable { - _command = SimpleAsyncCommand("Drive", 20, AdvancedArcadeJoystickInput( - maxVelocity, trackWidth, revBack, + _command = SimpleAsyncCommand("Drive", 20, AdvancedArcadeJoystickInput(revBack, DoubleSupplier { Utilities.scale(-Utilities.processDeadzone(joystick.getY(GenericHID.Hand.kLeft), 0.1), power) }, DoubleSupplier { Utilities.scale(Utilities.processDeadzone(joystick.getX(GenericHID.Hand.kRight), 0.1), power) }, DoubleSupplier { Utilities.scale((Utilities.processDeadzone(joystick.getTriggerAxis(GenericHID.Hand.kRight), 0.1) - Utilities.processDeadzone(joystick.getTriggerAxis(GenericHID.Hand.kLeft), 0.1)), power) }) + + FeedForwardToVelocityProcessor(maxVelocity) + (FeedForwardProcessor(1 / maxVelocity, 0.0, 0.0)) + UnitScaler(tpu, 0.1) + (CTREOutput(PipelineDriveTrain.left1, PipelineDriveTrain.right1))) - listOf(PipelineDriveTrain.left1, PipelineDriveTrain.right1).forEach { - it.configClosedloopRamp(ramp) - it.config_kP(0, p) - it.config_kI(0, i) - it.config_kD(0, d) - it.config_kF(0, 0.0) + PipelineDriveTrain.masters { + configClosedloopRamp(ramp) + config_kP(0, p) + config_kI(0, i) + config_kD(0, d) + config_kF(0, 0.0) } }).apply { @@ -114,6 +119,263 @@ class AdvancedJoystickInputPipelineTestRobot : DrivePipelineTestRobot() { override val command get() = _command } +class HeadingPIDPipelineTestRobot : DrivePipelineTestRobot() { + + @JvmField + @Preference(persistent = false) + var p = 0.0 + @JvmField + @Preference(persistent = false) + var i = 0.0 + @JvmField + @Preference(persistent = false) + var d = 0.0 + @JvmField + @Preference(persistent = false) + var hdgSet = 0.0 + @JvmField + @Preference(persistent = false) + var invertSides = false + + private var headingPIDProcessor: HeadingPIDProcessor? = null + + override fun robotInit() { + PreferenceManager.getInstance().add(this) + val reset = SimpleCommand("reset", Executable { + headingPIDProcessor = HeadingPIDProcessor(p, i, d, + { Math.toRadians(PipelineNavx.navx.yaw.toDouble()) }, + false, invertSides) + _command = SimpleAsyncCommand("Drive", 20, + Input { + TankDriveData( + DriveData(OptionalDouble.empty()), + DriveData(OptionalDouble.empty()), + OptionalDouble.of(hdgSet), + OptionalDouble.empty()) + } + headingPIDProcessor!! + (CTREOutput(PipelineDriveTrain.left1, PipelineDriveTrain.right1)) + ) + + + }).apply { + setRunWhenDisabled(true) + start() + } + + SmartDashboard.putData(reset) + } + + override fun robotPeriodic() { + super.robotPeriodic() + + headingPIDProcessor?.error?.let { SmartDashboard.putNumber("Error", it) } + headingPIDProcessor?.iAccum?.let { SmartDashboard.putNumber("Iaccum", it) } + SmartDashboard.putNumber("hdg", Math.toRadians(PipelineNavx.navx.yaw.toDouble())) + } + + private lateinit var _command: Command + override val command get() = _command +} + +class TurningRatePIDPipelineTestRobot : DrivePipelineTestRobot() { + + @JvmField + @Preference(persistent = false) + var p = 0.0 + @JvmField + @Preference(persistent = false) + var i = 0.0 + @JvmField + @Preference(persistent = false) + var d = 0.0 + @JvmField + @Preference(persistent = false) + var turnSet = 0.0 + @JvmField + @Preference(persistent = false) + var invertSides = false + + private var turningRatePIDProcessor: TurningRatePIDProcessor? = null + + override fun robotInit() { + PreferenceManager.getInstance().add(this) + val reset = SimpleCommand("reset", Executable { + turningRatePIDProcessor = TurningRatePIDProcessor({ Math.toRadians(PipelineNavx.navx.rate) }, p, i, d, invertSides) + _command = SimpleAsyncCommand("Drive", 20, + Input { + TankDriveData( + DriveData(OptionalDouble.empty()), + DriveData(OptionalDouble.empty()), + OptionalDouble.empty(), + OptionalDouble.of(turnSet)) + } + turningRatePIDProcessor!! + FeedForwardProcessor(1.0, 0.0, 0.0) + (CTREOutput(PipelineDriveTrain.left1, PipelineDriveTrain.right1)) + ) + + + }).apply { + setRunWhenDisabled(true) + start() + } + + SmartDashboard.putData(reset) + } + + override fun robotPeriodic() { + super.robotPeriodic() + + turningRatePIDProcessor?.error?.let { SmartDashboard.putNumber("Error", it) } + turningRatePIDProcessor?.iAccum?.let { SmartDashboard.putNumber("Iaccum", it) } + SmartDashboard.putNumber("hdg", Math.toRadians(PipelineNavx.navx.yaw.toDouble())) + } + + private lateinit var _command: Command + override val command get() = _command +} + +class MotionProfilePipelineTestRobot : DrivePipelineTestRobot() { + @JvmField + @Preference(persistent = false) + var hdgP = 0.0 + @JvmField + @Preference(persistent = false) + var hdgI = 0.0 + @JvmField + @Preference(persistent = false) + var hdgD = 0.0 + @JvmField + @Preference(persistent = false) + var driveP = 0.0 + @JvmField + @Preference(persistent = false) + var driveI = 0.0 + @JvmField + @Preference(persistent = false) + var driveD = 0.0 + @JvmField + @Preference(persistent = false) + var invertSides = false + @JvmField + @Preference(persistent = false) + var kV = 0.0 + @JvmField + @Preference(persistent = false) + var kA = 0.0 + @JvmField + @Preference(persistent = false) + var vIntercept = 0.0 + @JvmField + @Preference(persistent = false) + var profileName = "" + @JvmField + @Preference(persistent = false) + var tpu = 1.0 + @JvmField + @Preference(persistent = false) + var ramp = 0.0 + + private var hdgPIDProcessor: HeadingPIDProcessor? = null + private var profileInput: ProfileInput? = null + private var hdgTgt = 0.0 + private var lastProf: TankDriveData? = null + + private var notifier: Notifier? = null + + override fun robotInit() { + PreferenceManager.getInstance().add(this) + val reset = SimpleCommand("reset", Executable { + val container = ProfileContainer(File("/home/lvuser/roostertest")) + println("Loaded profiles: ${container.profileNames}") + + val profile = container[profileName] + if (profile == null) { + System.err.println("Motion profile not found, aborting reset") + return@Executable + } + + profileInput = ProfileInput(profile.left, profile.right) + + PipelineDriveTrain.masters { setSelectedSensorPosition(0) } + PipelineNavx.navx.zeroYaw() + + hdgPIDProcessor = HeadingPIDProcessor(hdgP, hdgI, hdgD, { Math.toRadians(PipelineNavx.navx.yaw.toDouble()) }, true, invertSides) + _command = SimpleAsyncCommand("Drive", 20, profileInput!! + + FeedForwardProcessor(kV, vIntercept, kA) + + hdgPIDProcessor!! + + Processor { lastProf = it; hdgTgt = it.heading.asDouble;it } + + UnitScaler(tpu, 0.1) + + CTREOutput(PipelineDriveTrain.left1, PipelineDriveTrain.right1) + ) + + PipelineDriveTrain.masters { + config_kP(0, driveP) + config_kI(0, driveI) + config_kD(0, driveD) + config_kF(0, 0.0) + } + PipelineDriveTrain.all { + configClosedloopRamp(ramp) + } + + }).apply { + setRunWhenDisabled(true) + start() + } + + SmartDashboard.putData(reset) + } + + private lateinit var _command: Command + override val command get() = _command + + override fun robotPeriodic() { + super.robotPeriodic() + + hdgPIDProcessor?.let { + SmartDashboard.putNumber("HDG ERR", Math.toDegrees(it.error)) + SmartDashboard.putNumber("HDG IACC", it.iAccum) + } + + lastProf?.let { + SmartDashboard.putNumber("LPOSTGT", it.left.position.orElse(0.0)) + SmartDashboard.putNumber("LVELTGT", it.left.velocity.orElse(0.0)) + SmartDashboard.putNumber("LACCTGT", it.left.acceleration.orElse(0.0)) + SmartDashboard.putNumber("RPOSTGT", it.right.position.orElse(0.0)) + SmartDashboard.putNumber("RVELTGT", it.right.velocity.orElse(0.0)) + SmartDashboard.putNumber("RACCTGT", it.right.acceleration.orElse(0.0)) + SmartDashboard.putNumber("HTGT", Math.toDegrees(hdgTgt)) + } + + SmartDashboard.putNumber("HDG", PipelineNavx.navx.yaw.toDouble()) + SmartDashboard.putNumber("LPOS", PipelineDriveTrain.left1.selectedSensorPosition / tpu) + SmartDashboard.putNumber("RPOS", PipelineDriveTrain.right1.selectedSensorPosition / tpu) + SmartDashboard.putNumber("LVEL", (PipelineDriveTrain.left1.selectedSensorVelocity * 10) / tpu) + SmartDashboard.putNumber("RVEL", (PipelineDriveTrain.right1.selectedSensorVelocity * 10) / tpu) + SmartDashboard.putNumber("LERR", PipelineDriveTrain.left1.closedLoopError / tpu) + SmartDashboard.putNumber("RERR", PipelineDriveTrain.right1.closedLoopError / tpu) + } + + override fun teleopInit() { + super.teleopInit() + + PipelineDriveTrain.masters { setSelectedSensorPosition(0) } + PipelineNavx.navx.zeroYaw() + + PipelineDriveTrain.all { + setBrake(true) + } + } + + override fun disabledInit() { + super.disabledInit() + notifier = Notifier { + Thread.sleep(1000) + PipelineDriveTrain.all { + setBrake(false) + } + } + notifier?.startSingle(1.0) + } +} + /** * Common drive train object to be used by all pipeline test robots. */ @@ -181,6 +443,18 @@ private object PipelineDriveTrain { inverted = true set(ControlMode.Follower, right1.deviceID.toDouble()) } + + fun all(block: ChickenTalon.() -> Unit) { + for (talon in listOf(left1, left2, left3, right1, right2, right3)) { + talon.block() + } + } + + fun masters(block: ChickenTalon.() -> Unit) { + for (talon in listOf(left1, right1)) { + talon.block() + } + } } /** diff --git a/src/testbots/kotlin/org/team1540/rooster/testing/ProfileContainerTestingRobot.kt b/src/testbots/kotlin/org/team1540/rooster/testing/ProfileContainerTestingRobot.kt new file mode 100644 index 00000000..a422f917 --- /dev/null +++ b/src/testbots/kotlin/org/team1540/rooster/testing/ProfileContainerTestingRobot.kt @@ -0,0 +1,18 @@ +package org.team1540.rooster.testing + +import edu.wpi.first.wpilibj.TimedRobot +import org.team1540.rooster.motionprofiling.ProfileContainer +import java.io.File +import kotlin.system.measureTimeMillis + +class ProfileContainerTestingRobot : TimedRobot() { + private lateinit var container: ProfileContainer + + override fun robotInit() { + val time = measureTimeMillis { + container = ProfileContainer(File("/home/lvuser/roostertest")) + } + println("Initialized profile container in $time ms") + println("Current profiles loaded: ${container.profileNames}") + } +} diff --git a/travis-scripts/name-release-files.sh b/travis-scripts/name-release-files.sh old mode 100644 new mode 100755