diff --git a/.github/workflows/comment-command.yml b/.github/workflows/comment-command.yml
index 6cdcf123bdb..8277499c38f 100644
--- a/.github/workflows/comment-command.yml
+++ b/.github/workflows/comment-command.yml
@@ -43,7 +43,7 @@ jobs:
distribution: 'zulu'
java-version: 17
- name: Install wpiformat
- run: pip3 install wpiformat==2024.33
+ run: pip3 install wpiformat==2024.34
- name: Run wpiformat
run: wpiformat
- name: Run spotlessApply
diff --git a/.github/workflows/lint-format.yml b/.github/workflows/lint-format.yml
index d32d9f70d00..b749a504920 100644
--- a/.github/workflows/lint-format.yml
+++ b/.github/workflows/lint-format.yml
@@ -27,7 +27,7 @@ jobs:
with:
python-version: '3.10'
- name: Install wpiformat
- run: pip3 install wpiformat==2024.33
+ run: pip3 install wpiformat==2024.34
- name: Run
run: wpiformat
- name: Check output
@@ -66,7 +66,7 @@ jobs:
with:
python-version: '3.10'
- name: Install wpiformat
- run: pip3 install wpiformat
+ run: pip3 install wpiformat==2024.34
- name: Create compile_commands.json
run: |
./gradlew generateCompileCommands -Ptoolchain-optional-roboRio
diff --git a/apriltag/src/main/java/edu/wpi/first/apriltag/AprilTagFields.java b/apriltag/src/main/java/edu/wpi/first/apriltag/AprilTagFields.java
index c569a9c55ff..016c982fe7e 100644
--- a/apriltag/src/main/java/edu/wpi/first/apriltag/AprilTagFields.java
+++ b/apriltag/src/main/java/edu/wpi/first/apriltag/AprilTagFields.java
@@ -35,7 +35,9 @@ public enum AprilTagFields {
*
* @return AprilTagFieldLayout of the field
* @throws UncheckedIOException If the layout does not exist
+ * @deprecated Use {@link AprilTagFieldLayout#loadField(AprilTagFields)} instead.
*/
+ @Deprecated(forRemoval = true, since = "2025")
public AprilTagFieldLayout loadAprilTagLayoutField() {
return AprilTagFieldLayout.loadField(this);
}
diff --git a/apriltag/src/main/native/include/frc/apriltag/AprilTagFieldLayout.h b/apriltag/src/main/native/include/frc/apriltag/AprilTagFieldLayout.h
index 6ba3824ed66..c2e73c78c4d 100644
--- a/apriltag/src/main/native/include/frc/apriltag/AprilTagFieldLayout.h
+++ b/apriltag/src/main/native/include/frc/apriltag/AprilTagFieldLayout.h
@@ -166,7 +166,9 @@ void from_json(const wpi::json& json, AprilTagFieldLayout& layout);
*
* @param field The predefined field
* @return AprilTagFieldLayout of the field
+ * @deprecated Use AprilTagFieldLayout::LoadField() instead
*/
+[[deprecated("Use AprilTagFieldLayout::LoadField() instead")]]
WPILIB_DLLEXPORT AprilTagFieldLayout
LoadAprilTagLayoutField(AprilTagField field);
diff --git a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/Command.java b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/Command.java
index 0186b6550d6..3a5ee405f63 100644
--- a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/Command.java
+++ b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/Command.java
@@ -349,11 +349,16 @@ public RepeatCommand repeatedly() {
}
/**
- * Decorates this command to run "by proxy" by wrapping it in a {@link ProxyCommand}. This is
- * useful for "forking off" from command compositions when the user does not wish to extend the
- * command's requirements to the entire command composition.
+ * Decorates this command to run "by proxy" by wrapping it in a {@link ProxyCommand}. Use this for
+ * "forking off" from command compositions when the user does not wish to extend the command's
+ * requirements to the entire command composition. ProxyCommand has unique implications and
+ * semantics, see the WPILib docs for a full explanation.
*
* @return the decorated command
+ * @see ProxyCommand
+ * @see WPILib
+ * docs
*/
public ProxyCommand asProxy() {
return new ProxyCommand(this);
diff --git a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/DeferredCommand.java b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/DeferredCommand.java
index 099eba3b04e..f76cf909238 100644
--- a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/DeferredCommand.java
+++ b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/DeferredCommand.java
@@ -11,9 +11,9 @@
import java.util.function.Supplier;
/**
- * Defers Command construction to runtime. Runs the command returned by the supplier when this
- * command is initialized, and ends when it ends. Useful for performing runtime tasks before
- * creating a new command. If this command is interrupted, it will cancel the command.
+ * Defers Command construction to runtime. Runs the command returned by a supplier when this command
+ * is initialized, and ends when it ends. Useful for performing runtime tasks before creating a new
+ * command. If this command is interrupted, it will cancel the command.
*
*
Note that the supplier must create a new Command each call. For selecting one of a
* preallocated set of commands, use {@link SelectCommand}.
@@ -28,9 +28,10 @@ public class DeferredCommand extends Command {
private Command m_command = m_nullCommand;
/**
- * Creates a new DeferredCommand that runs the supplied command when initialized, and ends when it
- * ends. Useful for lazily creating commands at runtime. The {@link Supplier} will be called each
- * time this command is initialized. The Supplier must create a new Command each call.
+ * Creates a new DeferredCommand that directly runs the supplied command when initialized, and
+ * ends when it ends. Useful for lazily creating commands when the DeferredCommand is initialized,
+ * such as if the supplied command depends on runtime state. The {@link Supplier} will be called
+ * each time this command is initialized. The Supplier must create a new Command each call.
*
* @param supplier The command supplier
* @param requirements The command requirements. This is a {@link Set} to prevent accidental
diff --git a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/ProxyCommand.java b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/ProxyCommand.java
index f05b286f4d4..f71c10b2339 100644
--- a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/ProxyCommand.java
+++ b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/ProxyCommand.java
@@ -10,8 +10,13 @@
import java.util.function.Supplier;
/**
- * Schedules the given command when this command is initialized, and ends when it ends. Useful for
- * forking off from CommandGroups. If this command is interrupted, it will cancel the command.
+ * Schedules a given command when this command is initialized and ends when it ends, but does not
+ * directly run it. Use this for including a command in a composition without adding its
+ * requirements, but only if you know what you are doing. If you are unsure, see the
+ * WPILib docs for a complete explanation of proxy semantics. Do not proxy a command
+ * from a subsystem already required by the composition, or else the composition will cancel itself
+ * when the proxy is reached. If this command is interrupted, it will cancel the command.
*
*
This class is provided by the NewCommands VendorDep
*/
@@ -21,9 +26,12 @@ public class ProxyCommand extends Command {
/**
* Creates a new ProxyCommand that schedules the supplied command when initialized, and ends when
- * it is no longer scheduled. Useful for lazily creating commands at runtime.
+ * it is no longer scheduled. Use this for lazily creating proxied commands at
+ * runtime. Proxying should only be done to escape from composition requirement semantics, so if
+ * only initialization time command construction is needed, use {@link DeferredCommand} instead.
*
* @param supplier the command supplier
+ * @see DeferredCommand
*/
public ProxyCommand(Supplier supplier) {
m_supplier = requireNonNullParam(supplier, "supplier", "ProxyCommand");
diff --git a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/TrapezoidProfileCommand.java b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/TrapezoidProfileCommand.java
index 3bfdbaebf52..8e49706958c 100644
--- a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/TrapezoidProfileCommand.java
+++ b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/TrapezoidProfileCommand.java
@@ -22,7 +22,6 @@ public class TrapezoidProfileCommand extends Command {
private final Consumer m_output;
private final Supplier m_goal;
private final Supplier m_currentState;
- private final boolean m_newAPI; // TODO: Remove
private final Timer m_timer = new Timer();
/**
@@ -46,29 +45,6 @@ public TrapezoidProfileCommand(
m_output = requireNonNullParam(output, "output", "TrapezoidProfileCommand");
m_goal = goal;
m_currentState = currentState;
- m_newAPI = true;
- addRequirements(requirements);
- }
-
- /**
- * Creates a new TrapezoidProfileCommand that will execute the given {@link TrapezoidProfile}.
- * Output will be piped to the provided consumer function.
- *
- * @param profile The motion profile to execute.
- * @param output The consumer for the profile output.
- * @param requirements The subsystems required by this command.
- * @deprecated The new constructor allows you to pass in a supplier for desired and current state.
- * This allows you to change goals at runtime.
- */
- @Deprecated(since = "2024", forRemoval = true)
- @SuppressWarnings("this-escape")
- public TrapezoidProfileCommand(
- TrapezoidProfile profile, Consumer output, Subsystem... requirements) {
- m_profile = requireNonNullParam(profile, "profile", "TrapezoidProfileCommand");
- m_output = requireNonNullParam(output, "output", "TrapezoidProfileCommand");
- m_newAPI = false;
- m_goal = null;
- m_currentState = null;
addRequirements(requirements);
}
@@ -80,11 +56,7 @@ public void initialize() {
@Override
@SuppressWarnings("removal")
public void execute() {
- if (m_newAPI) {
- m_output.accept(m_profile.calculate(m_timer.get(), m_currentState.get(), m_goal.get()));
- } else {
- m_output.accept(m_profile.calculate(m_timer.get()));
- }
+ m_output.accept(m_profile.calculate(m_timer.get(), m_currentState.get(), m_goal.get()));
}
@Override
diff --git a/wpilibNewCommands/src/main/native/cpp/frc2/command/ProxyCommand.cpp b/wpilibNewCommands/src/main/native/cpp/frc2/command/ProxyCommand.cpp
index eb4fb7cab1f..3851afa53c0 100644
--- a/wpilibNewCommands/src/main/native/cpp/frc2/command/ProxyCommand.cpp
+++ b/wpilibNewCommands/src/main/native/cpp/frc2/command/ProxyCommand.cpp
@@ -4,6 +4,7 @@
#include "frc2/command/ProxyCommand.h"
+#include
#include
using namespace frc2;
@@ -20,11 +21,11 @@ ProxyCommand::ProxyCommand(wpi::unique_function supplier)
ProxyCommand::ProxyCommand(Command* command)
: ProxyCommand([command] { return command; }) {
- SetName(std::string{"Proxy("}.append(command->GetName()).append(")"));
+ SetName(fmt::format("Proxy({})", command->GetName()));
}
ProxyCommand::ProxyCommand(std::unique_ptr command) {
- SetName(std::string{"Proxy("}.append(command->GetName()).append(")"));
+ SetName(fmt::format("Proxy({})", command->GetName()));
m_supplier = [command = std::move(command)] { return command.get(); };
}
diff --git a/wpilibNewCommands/src/main/native/include/frc2/command/Command.h b/wpilibNewCommands/src/main/native/include/frc2/command/Command.h
index ad6aa79040c..27ac7f35828 100644
--- a/wpilibNewCommands/src/main/native/include/frc2/command/Command.h
+++ b/wpilibNewCommands/src/main/native/include/frc2/command/Command.h
@@ -249,14 +249,17 @@ class Command : public wpi::Sendable, public wpi::SendableHelper {
CommandPtr Repeatedly() &&;
/**
- * Decorates this command to run "by proxy" by wrapping it in a
- * ProxyCommand. This is useful for "forking off" from command groups
- * when the user does not wish to extend the command's requirements to the
- * entire command group.
+ * Decorates this command to run "by proxy" by wrapping it in a ProxyCommand.
+ * Use this for "forking off" from command compositions when the user does not
+ * wish to extend the command's requirements to the entire command
+ * composition. ProxyCommand has unique implications and semantics, see the
+ * WPILib docs for a full explanation.
*
* This overload transfers command ownership to the returned CommandPtr.
*
* @return the decorated command
+ * @see ProxyCommand
*/
[[nodiscard]]
CommandPtr AsProxy() &&;
diff --git a/wpilibNewCommands/src/main/native/include/frc2/command/CommandPtr.h b/wpilibNewCommands/src/main/native/include/frc2/command/CommandPtr.h
index 2610684b09e..f4548e4438f 100644
--- a/wpilibNewCommands/src/main/native/include/frc2/command/CommandPtr.h
+++ b/wpilibNewCommands/src/main/native/include/frc2/command/CommandPtr.h
@@ -7,7 +7,6 @@
#include
#include
#include
-#include
#include
#include
#include
@@ -51,12 +50,15 @@ class CommandPtr final {
CommandPtr Repeatedly() &&;
/**
- * Decorates this command to run "by proxy" by wrapping it in a
- * ProxyCommand. This is useful for "forking off" from command groups
- * when the user does not wish to extend the command's requirements to the
- * entire command group.
+ * Decorates this command to run "by proxy" by wrapping it in a ProxyCommand.
+ * Use this for "forking off" from command compositions when the user does not
+ * wish to extend the command's requirements to the entire command
+ * composition. ProxyCommand has unique implications and semantics, see the
+ * WPILib docs for a full explanation.
*
* @return the decorated command
+ * @see ProxyCommand
*/
[[nodiscard]]
CommandPtr AsProxy() &&;
diff --git a/wpilibNewCommands/src/main/native/include/frc2/command/DeferredCommand.h b/wpilibNewCommands/src/main/native/include/frc2/command/DeferredCommand.h
index 442076d8f27..28460fdf9b5 100644
--- a/wpilibNewCommands/src/main/native/include/frc2/command/DeferredCommand.h
+++ b/wpilibNewCommands/src/main/native/include/frc2/command/DeferredCommand.h
@@ -16,7 +16,7 @@
namespace frc2 {
/**
- * Defers Command construction to runtime. Runs the command returned by the
+ * Defers Command construction to runtime. Runs the command returned by a
* supplier when this command is initialized, and ends when it ends. Useful for
* performing runtime tasks before creating a new command. If this command is
* interrupted, it will cancel the command.
@@ -29,9 +29,10 @@ namespace frc2 {
class DeferredCommand : public CommandHelper {
public:
/**
- * Creates a new DeferredCommand that runs the supplied command when
- * initialized, and ends when it ends. Useful for lazily
- * creating commands at runtime. The supplier will be called each time this
+ * Creates a new DeferredCommand that directly runs the supplied command when
+ * initialized, and ends when it ends. Useful for lazily creating commands
+ * when the DeferredCommand is initialized, such as if the supplied command
+ * depends on runtime state. The supplier will be called each time this
* command is initialized. The supplier must create a new Command each
* call.
*
diff --git a/wpilibNewCommands/src/main/native/include/frc2/command/ParallelDeadlineGroup.h b/wpilibNewCommands/src/main/native/include/frc2/command/ParallelDeadlineGroup.h
index 9e253fbffdf..22e9759f2b4 100644
--- a/wpilibNewCommands/src/main/native/include/frc2/command/ParallelDeadlineGroup.h
+++ b/wpilibNewCommands/src/main/native/include/frc2/command/ParallelDeadlineGroup.h
@@ -9,7 +9,6 @@
#pragma warning(disable : 4521)
#endif
-#include
#include
#include
#include
diff --git a/wpilibNewCommands/src/main/native/include/frc2/command/ParallelRaceGroup.h b/wpilibNewCommands/src/main/native/include/frc2/command/ParallelRaceGroup.h
index caefa48fa35..17db907dd43 100644
--- a/wpilibNewCommands/src/main/native/include/frc2/command/ParallelRaceGroup.h
+++ b/wpilibNewCommands/src/main/native/include/frc2/command/ParallelRaceGroup.h
@@ -9,7 +9,6 @@
#pragma warning(disable : 4521)
#endif
-#include
#include
#include
#include
diff --git a/wpilibNewCommands/src/main/native/include/frc2/command/ProxyCommand.h b/wpilibNewCommands/src/main/native/include/frc2/command/ProxyCommand.h
index 3b7eecc6741..d4d4757cf4f 100644
--- a/wpilibNewCommands/src/main/native/include/frc2/command/ProxyCommand.h
+++ b/wpilibNewCommands/src/main/native/include/frc2/command/ProxyCommand.h
@@ -13,9 +13,15 @@
namespace frc2 {
/**
- * Schedules the given command when this command is initialized, and ends when
- * it ends. Useful for forking off from CommandGroups. If this command is
- * interrupted, it will cancel the command.
+ * Schedules a given command when this command is initialized and ends when it
+ * ends, but does not directly run it. Use this for including a command in a
+ * composition without adding its requirements, but only if you know
+ * what you are doing. If you are unsure, see the
+ * WPILib docs for a complete explanation of proxy semantics. Do
+ * not proxy a command from a subsystem already required by the composition, or
+ * else the composition will cancel itself when the proxy is reached. If this
+ * command is interrupted, it will cancel the command.
*
* This class is provided by the NewCommands VendorDep
*/
@@ -23,19 +29,27 @@ class ProxyCommand : public CommandHelper {
public:
/**
* Creates a new ProxyCommand that schedules the supplied command when
- * initialized, and ends when it is no longer scheduled. Useful for lazily
- * creating commands at runtime.
+ * initialized, and ends when it is no longer scheduled. Use this for lazily
+ * creating proxied commands at runtime. Proxying should only
+ * be done to escape from composition requirement semantics, so if only
+ * initialization time command construction is needed, use {@link
+ * DeferredCommand} instead.
*
* @param supplier the command supplier
+ * @see DeferredCommand
*/
explicit ProxyCommand(wpi::unique_function supplier);
/**
* Creates a new ProxyCommand that schedules the supplied command when
- * initialized, and ends when it is no longer scheduled. Useful for lazily
- * creating commands at runtime.
+ * initialized, and ends when it is no longer scheduled. Use this for lazily
+ * creating proxied commands at runtime. Proxying should only
+ * be done to escape from composition requirement semantics, so if only
+ * initialization time command construction is needed, use {@link
+ * DeferredCommand} instead.
*
* @param supplier the command supplier
+ * @see DeferredCommand
*/
explicit ProxyCommand(wpi::unique_function supplier);
diff --git a/wpilibNewCommands/src/main/native/include/frc2/command/SequentialCommandGroup.h b/wpilibNewCommands/src/main/native/include/frc2/command/SequentialCommandGroup.h
index fdcd1bc22fc..321118880b4 100644
--- a/wpilibNewCommands/src/main/native/include/frc2/command/SequentialCommandGroup.h
+++ b/wpilibNewCommands/src/main/native/include/frc2/command/SequentialCommandGroup.h
@@ -9,7 +9,6 @@
#pragma warning(disable : 4521)
#endif
-#include
#include
#include
#include
diff --git a/wpilibNewCommands/src/main/native/include/frc2/command/TrapezoidProfileCommand.h b/wpilibNewCommands/src/main/native/include/frc2/command/TrapezoidProfileCommand.h
index 837dda3e098..30ad9b70872 100644
--- a/wpilibNewCommands/src/main/native/include/frc2/command/TrapezoidProfileCommand.h
+++ b/wpilibNewCommands/src/main/native/include/frc2/command/TrapezoidProfileCommand.h
@@ -52,28 +52,6 @@ class TrapezoidProfileCommand
m_goal(goal),
m_currentState(currentState) {
this->AddRequirements(requirements);
- m_newAPI = true;
- }
-
- /**
- * Creates a new TrapezoidProfileCommand that will execute the given
- * TrapezoidalProfile. Output will be piped to the provided consumer function.
- *
- * @param profile The motion profile to execute.
- * @param output The consumer for the profile output.
- * @param requirements The list of requirements.
- * @deprecated The new constructor allows you to pass in a supplier for
- * desired and current state. This allows you to change goals at runtime.
- */
- [[deprecated(
- "The new constructor allows you to pass in a supplier for desired and "
- "current state. This allows you to change goals at runtime.")]]
- TrapezoidProfileCommand(frc::TrapezoidProfile profile,
- std::function output,
- Requirements requirements = {})
- : m_profile(profile), m_output(output) {
- this->AddRequirements(requirements);
- m_newAPI = false;
}
void Initialize() override { m_timer.Restart(); }
@@ -93,7 +71,6 @@ class TrapezoidProfileCommand
std::function m_output;
std::function m_goal;
std::function m_currentState;
- bool m_newAPI; // TODO: Remove
frc::Timer m_timer;
};
diff --git a/wpilibNewCommands/src/test/native/cpp/frc2/command/CommandDecoratorTest.cpp b/wpilibNewCommands/src/test/native/cpp/frc2/command/CommandDecoratorTest.cpp
index 884d5043469..7c342476fb1 100644
--- a/wpilibNewCommands/src/test/native/cpp/frc2/command/CommandDecoratorTest.cpp
+++ b/wpilibNewCommands/src/test/native/cpp/frc2/command/CommandDecoratorTest.cpp
@@ -5,12 +5,9 @@
#include
#include "CommandTestBase.h"
-#include "frc2/command/ConditionalCommand.h"
#include "frc2/command/FunctionalCommand.h"
#include "frc2/command/InstantCommand.h"
-#include "frc2/command/ParallelRaceGroup.h"
#include "frc2/command/RunCommand.h"
-#include "frc2/command/SequentialCommandGroup.h"
using namespace frc2;
class CommandDecoratorTest : public CommandTestBase {};
diff --git a/wpilibNewCommands/src/test/native/cpp/frc2/command/CommandTestBase.h b/wpilibNewCommands/src/test/native/cpp/frc2/command/CommandTestBase.h
index 586432dff05..16d0cdc8b2c 100644
--- a/wpilibNewCommands/src/test/native/cpp/frc2/command/CommandTestBase.h
+++ b/wpilibNewCommands/src/test/native/cpp/frc2/command/CommandTestBase.h
@@ -5,7 +5,6 @@
#pragma once
#include
-#include
#include
#include
@@ -16,14 +15,13 @@
#include "frc2/command/Requirements.h"
#include "frc2/command/SubsystemBase.h"
#include "gmock/gmock.h"
-#include "make_vector.h"
namespace frc2 {
class TestSubsystem : public SubsystemBase {
public:
explicit TestSubsystem(std::function periodic = [] {})
- : m_periodic{periodic} {}
+ : m_periodic{std::move(periodic)} {}
void Periodic() override { m_periodic(); }
private:
diff --git a/wpimath/src/main/java/edu/wpi/first/math/estimator/PoseEstimator.java b/wpimath/src/main/java/edu/wpi/first/math/estimator/PoseEstimator.java
index 14eee8f190a..141844e362e 100644
--- a/wpimath/src/main/java/edu/wpi/first/math/estimator/PoseEstimator.java
+++ b/wpimath/src/main/java/edu/wpi/first/math/estimator/PoseEstimator.java
@@ -21,6 +21,7 @@
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
+import java.util.Optional;
/**
* This class wraps {@link Odometry} to fuse latency-compensated vision measurements with encoder
@@ -125,6 +126,16 @@ public Pose2d getEstimatedPosition() {
return m_odometry.getPoseMeters();
}
+ /**
+ * Return the pose at a given timestamp, if the buffer is not empty.
+ *
+ * @param timestampSeconds The pose's timestamp in seconds.
+ * @return The pose at the given timestamp (or Optional.empty() if the buffer is empty).
+ */
+ public Optional sampleAt(double timestampSeconds) {
+ return m_poseBuffer.getSample(timestampSeconds).map(record -> record.poseMeters);
+ }
+
/**
* Adds a vision measurement to the Kalman Filter. This will correct the odometry pose estimate
* while still accounting for measurement noise.
diff --git a/wpimath/src/main/native/include/frc/estimator/PoseEstimator.h b/wpimath/src/main/native/include/frc/estimator/PoseEstimator.h
index 6e38d8c65fb..342ed2529c9 100644
--- a/wpimath/src/main/native/include/frc/estimator/PoseEstimator.h
+++ b/wpimath/src/main/native/include/frc/estimator/PoseEstimator.h
@@ -4,6 +4,8 @@
#pragma once
+#include
+
#include
#include
#include
@@ -89,6 +91,15 @@ class WPILIB_DLLEXPORT PoseEstimator {
*/
Pose2d GetEstimatedPosition() const;
+ /**
+ * Return the pose at a given timestamp, if the buffer is not empty.
+ *
+ * @param timestamp The pose's timestamp.
+ * @return The pose at the given timestamp (or std::nullopt if the buffer is
+ * empty).
+ */
+ std::optional SampleAt(units::second_t timestamp) const;
+
/**
* Adds a vision measurement to the Kalman Filter. This will correct
* the odometry pose estimate while still accounting for measurement noise.
diff --git a/wpimath/src/main/native/include/frc/estimator/PoseEstimator.inc b/wpimath/src/main/native/include/frc/estimator/PoseEstimator.inc
index 79d71bcc566..43942cfbae2 100644
--- a/wpimath/src/main/native/include/frc/estimator/PoseEstimator.inc
+++ b/wpimath/src/main/native/include/frc/estimator/PoseEstimator.inc
@@ -57,6 +57,19 @@ Pose2d PoseEstimator::GetEstimatedPosition()
return m_odometry.GetPose();
}
+template
+std::optional PoseEstimator::SampleAt(
+ units::second_t timestamp) const {
+ // TODO Replace with std::optional::transform() in C++23
+ std::optional::InterpolationRecord>
+ sample = m_poseBuffer.Sample(timestamp);
+ if (sample) {
+ return sample->pose;
+ } else {
+ return std::nullopt;
+ }
+}
+
template
void PoseEstimator::AddVisionMeasurement(
const Pose2d& visionRobotPose, units::second_t timestamp) {
diff --git a/wpimath/src/main/native/include/frc/interpolation/TimeInterpolatableBuffer.h b/wpimath/src/main/native/include/frc/interpolation/TimeInterpolatableBuffer.h
index 94d28180d8f..bfb1b548976 100644
--- a/wpimath/src/main/native/include/frc/interpolation/TimeInterpolatableBuffer.h
+++ b/wpimath/src/main/native/include/frc/interpolation/TimeInterpolatableBuffer.h
@@ -100,7 +100,7 @@ class TimeInterpolatableBuffer {
*
* @param time The time at which to sample the buffer.
*/
- std::optional Sample(units::second_t time) {
+ std::optional Sample(units::second_t time) const {
if (m_pastSnapshots.empty()) {
return {};
}
diff --git a/wpimath/src/test/java/edu/wpi/first/math/estimator/DifferentialDrivePoseEstimatorTest.java b/wpimath/src/test/java/edu/wpi/first/math/estimator/DifferentialDrivePoseEstimatorTest.java
index 08c5a156cf1..b019c608116 100644
--- a/wpimath/src/test/java/edu/wpi/first/math/estimator/DifferentialDrivePoseEstimatorTest.java
+++ b/wpimath/src/test/java/edu/wpi/first/math/estimator/DifferentialDrivePoseEstimatorTest.java
@@ -18,6 +18,7 @@
import edu.wpi.first.math.trajectory.TrajectoryConfig;
import edu.wpi.first.math.trajectory.TrajectoryGenerator;
import java.util.List;
+import java.util.Optional;
import java.util.Random;
import java.util.TreeMap;
import java.util.function.Function;
@@ -304,4 +305,54 @@ void testDiscardsStaleVisionMeasurements() {
estimator.getEstimatedPosition().getRotation().getRadians(),
"Incorrect Final Theta");
}
+
+ @Test
+ void testSampleAt() {
+ var kinematics = new DifferentialDriveKinematics(1);
+ var estimator =
+ new DifferentialDrivePoseEstimator(
+ kinematics,
+ new Rotation2d(),
+ 0,
+ 0,
+ new Pose2d(),
+ VecBuilder.fill(1, 1, 1),
+ VecBuilder.fill(1, 1, 1));
+
+ // Returns empty when null
+ assertEquals(Optional.empty(), estimator.sampleAt(1));
+
+ // Add odometry measurements, but don't fill up the buffer
+ // Add a tiny tolerance for the upper bound because of floating point rounding error
+ for (double time = 1; time <= 2 + 1e-9; time += 0.02) {
+ estimator.updateWithTime(time, new Rotation2d(), time, time);
+ }
+
+ // Sample at an added time
+ assertEquals(Optional.of(new Pose2d(1.02, 0, new Rotation2d())), estimator.sampleAt(1.02));
+ // Sample between updates (test interpolation)
+ assertEquals(Optional.of(new Pose2d(1.01, 0, new Rotation2d())), estimator.sampleAt(1.01));
+ // Sampling before the oldest value returns the oldest value
+ assertEquals(Optional.of(new Pose2d(1, 0, new Rotation2d())), estimator.sampleAt(0.5));
+ // Sampling after the newest value returns the newest value
+ assertEquals(Optional.of(new Pose2d(2, 0, new Rotation2d())), estimator.sampleAt(2.5));
+
+ // Add a vision measurement after the odometry measurements (while keeping all of the old
+ // odometry measurements)
+ estimator.addVisionMeasurement(new Pose2d(2, 0, new Rotation2d(1)), 2.2);
+
+ // Make sure nothing changed (except the newest value)
+ assertEquals(Optional.of(new Pose2d(1.02, 0, new Rotation2d())), estimator.sampleAt(1.02));
+ assertEquals(Optional.of(new Pose2d(1.01, 0, new Rotation2d())), estimator.sampleAt(1.01));
+ assertEquals(Optional.of(new Pose2d(1, 0, new Rotation2d())), estimator.sampleAt(0.5));
+
+ // Add a vision measurement before the odometry measurements that's still in the buffer
+ estimator.addVisionMeasurement(new Pose2d(1, 0.2, new Rotation2d()), 0.9);
+
+ // Everything should be the same except Y is 0.1 (halfway between 0 and 0.2)
+ assertEquals(Optional.of(new Pose2d(1.02, 0.1, new Rotation2d())), estimator.sampleAt(1.02));
+ assertEquals(Optional.of(new Pose2d(1.01, 0.1, new Rotation2d())), estimator.sampleAt(1.01));
+ assertEquals(Optional.of(new Pose2d(1, 0.1, new Rotation2d())), estimator.sampleAt(0.5));
+ assertEquals(Optional.of(new Pose2d(2, 0.1, new Rotation2d())), estimator.sampleAt(2.5));
+ }
}
diff --git a/wpimath/src/test/java/edu/wpi/first/math/estimator/MecanumDrivePoseEstimatorTest.java b/wpimath/src/test/java/edu/wpi/first/math/estimator/MecanumDrivePoseEstimatorTest.java
index ea08a5f1d90..c48012f7f95 100644
--- a/wpimath/src/test/java/edu/wpi/first/math/estimator/MecanumDrivePoseEstimatorTest.java
+++ b/wpimath/src/test/java/edu/wpi/first/math/estimator/MecanumDrivePoseEstimatorTest.java
@@ -19,6 +19,7 @@
import edu.wpi.first.math.trajectory.TrajectoryConfig;
import edu.wpi.first.math.trajectory.TrajectoryGenerator;
import java.util.List;
+import java.util.Optional;
import java.util.Random;
import java.util.TreeMap;
import java.util.function.Function;
@@ -321,4 +322,59 @@ void testDiscardsOldVisionMeasurements() {
estimator.getEstimatedPosition().getRotation().getRadians(),
"Incorrect Final Theta");
}
+
+ @Test
+ void testSampleAt() {
+ var kinematics =
+ new MecanumDriveKinematics(
+ new Translation2d(1, 1),
+ new Translation2d(-1, 1),
+ new Translation2d(1, -1),
+ new Translation2d(-1, -1));
+ var estimator =
+ new MecanumDrivePoseEstimator(
+ kinematics,
+ new Rotation2d(),
+ new MecanumDriveWheelPositions(),
+ new Pose2d(),
+ VecBuilder.fill(1, 1, 1),
+ VecBuilder.fill(1, 1, 1));
+
+ // Returns empty when null
+ assertEquals(Optional.empty(), estimator.sampleAt(1));
+
+ // Add odometry measurements, but don't fill up the buffer
+ // Add a tiny tolerance for the upper bound because of floating point rounding error
+ for (double time = 1; time <= 2 + 1e-9; time += 0.02) {
+ var wheelPositions = new MecanumDriveWheelPositions(time, time, time, time);
+ estimator.updateWithTime(time, new Rotation2d(), wheelPositions);
+ }
+
+ // Sample at an added time
+ assertEquals(Optional.of(new Pose2d(1.02, 0, new Rotation2d())), estimator.sampleAt(1.02));
+ // Sample between updates (test interpolation)
+ assertEquals(Optional.of(new Pose2d(1.01, 0, new Rotation2d())), estimator.sampleAt(1.01));
+ // Sampling before the oldest value returns the oldest value
+ assertEquals(Optional.of(new Pose2d(1, 0, new Rotation2d())), estimator.sampleAt(0.5));
+ // Sampling after the newest value returns the newest value
+ assertEquals(Optional.of(new Pose2d(2, 0, new Rotation2d())), estimator.sampleAt(2.5));
+
+ // Add a vision measurement after the odometry measurements (while keeping all of the old
+ // odometry measurements)
+ estimator.addVisionMeasurement(new Pose2d(2, 0, new Rotation2d(1)), 2.2);
+
+ // Make sure nothing changed (except the newest value)
+ assertEquals(Optional.of(new Pose2d(1.02, 0, new Rotation2d())), estimator.sampleAt(1.02));
+ assertEquals(Optional.of(new Pose2d(1.01, 0, new Rotation2d())), estimator.sampleAt(1.01));
+ assertEquals(Optional.of(new Pose2d(1, 0, new Rotation2d())), estimator.sampleAt(0.5));
+
+ // Add a vision measurement before the odometry measurements that's still in the buffer
+ estimator.addVisionMeasurement(new Pose2d(1, 0.2, new Rotation2d()), 0.9);
+
+ // Everything should be the same except Y is 0.1 (halfway between 0 and 0.2)
+ assertEquals(Optional.of(new Pose2d(1.02, 0.1, new Rotation2d())), estimator.sampleAt(1.02));
+ assertEquals(Optional.of(new Pose2d(1.01, 0.1, new Rotation2d())), estimator.sampleAt(1.01));
+ assertEquals(Optional.of(new Pose2d(1, 0.1, new Rotation2d())), estimator.sampleAt(0.5));
+ assertEquals(Optional.of(new Pose2d(2, 0.1, new Rotation2d())), estimator.sampleAt(2.5));
+ }
}
diff --git a/wpimath/src/test/java/edu/wpi/first/math/estimator/SwerveDrivePoseEstimatorTest.java b/wpimath/src/test/java/edu/wpi/first/math/estimator/SwerveDrivePoseEstimatorTest.java
index f827a102527..7101fa554a8 100644
--- a/wpimath/src/test/java/edu/wpi/first/math/estimator/SwerveDrivePoseEstimatorTest.java
+++ b/wpimath/src/test/java/edu/wpi/first/math/estimator/SwerveDrivePoseEstimatorTest.java
@@ -19,6 +19,7 @@
import edu.wpi.first.math.trajectory.TrajectoryConfig;
import edu.wpi.first.math.trajectory.TrajectoryGenerator;
import java.util.List;
+import java.util.Optional;
import java.util.Random;
import java.util.TreeMap;
import java.util.function.Function;
@@ -355,4 +356,70 @@ void testDiscardsOldVisionMeasurements() {
estimator.getEstimatedPosition().getRotation().getRadians(),
"Incorrect Final Theta");
}
+
+ @Test
+ void testSampleAt() {
+ var kinematics =
+ new SwerveDriveKinematics(
+ new Translation2d(1, 1),
+ new Translation2d(-1, 1),
+ new Translation2d(1, -1),
+ new Translation2d(-1, -1));
+ var estimator =
+ new SwerveDrivePoseEstimator(
+ kinematics,
+ new Rotation2d(),
+ new SwerveModulePosition[] {
+ new SwerveModulePosition(),
+ new SwerveModulePosition(),
+ new SwerveModulePosition(),
+ new SwerveModulePosition()
+ },
+ new Pose2d(),
+ VecBuilder.fill(1, 1, 1),
+ VecBuilder.fill(1, 1, 1));
+
+ // Returns empty when null
+ assertEquals(Optional.empty(), estimator.sampleAt(1));
+
+ // Add odometry measurements, but don't fill up the buffer
+ // Add a tiny tolerance for the upper bound because of floating point rounding error
+ for (double time = 1; time <= 2 + 1e-9; time += 0.02) {
+ var wheelPositions =
+ new SwerveModulePosition[] {
+ new SwerveModulePosition(time, new Rotation2d()),
+ new SwerveModulePosition(time, new Rotation2d()),
+ new SwerveModulePosition(time, new Rotation2d()),
+ new SwerveModulePosition(time, new Rotation2d())
+ };
+ estimator.updateWithTime(time, new Rotation2d(), wheelPositions);
+ }
+
+ // Sample at an added time
+ assertEquals(Optional.of(new Pose2d(1.02, 0, new Rotation2d())), estimator.sampleAt(1.02));
+ // Sample between updates (test interpolation)
+ assertEquals(Optional.of(new Pose2d(1.01, 0, new Rotation2d())), estimator.sampleAt(1.01));
+ // Sampling before the oldest value returns the oldest value
+ assertEquals(Optional.of(new Pose2d(1, 0, new Rotation2d())), estimator.sampleAt(0.5));
+ // Sampling after the newest value returns the newest value
+ assertEquals(Optional.of(new Pose2d(2, 0, new Rotation2d())), estimator.sampleAt(2.5));
+
+ // Add a vision measurement after the odometry measurements (while keeping all of the old
+ // odometry measurements)
+ estimator.addVisionMeasurement(new Pose2d(2, 0, new Rotation2d(1)), 2.2);
+
+ // Make sure nothing changed (except the newest value)
+ assertEquals(Optional.of(new Pose2d(1.02, 0, new Rotation2d())), estimator.sampleAt(1.02));
+ assertEquals(Optional.of(new Pose2d(1.01, 0, new Rotation2d())), estimator.sampleAt(1.01));
+ assertEquals(Optional.of(new Pose2d(1, 0, new Rotation2d())), estimator.sampleAt(0.5));
+
+ // Add a vision measurement before the odometry measurements that's still in the buffer
+ estimator.addVisionMeasurement(new Pose2d(1, 0.2, new Rotation2d()), 0.9);
+
+ // Everything should be the same except Y is 0.1 (halfway between 0 and 0.2)
+ assertEquals(Optional.of(new Pose2d(1.02, 0.1, new Rotation2d())), estimator.sampleAt(1.02));
+ assertEquals(Optional.of(new Pose2d(1.01, 0.1, new Rotation2d())), estimator.sampleAt(1.01));
+ assertEquals(Optional.of(new Pose2d(1, 0.1, new Rotation2d())), estimator.sampleAt(0.5));
+ assertEquals(Optional.of(new Pose2d(2, 0.1, new Rotation2d())), estimator.sampleAt(2.5));
+ }
}
diff --git a/wpimath/src/test/native/cpp/estimator/DifferentialDrivePoseEstimatorTest.cpp b/wpimath/src/test/native/cpp/estimator/DifferentialDrivePoseEstimatorTest.cpp
index 3103068344d..8144700e86b 100644
--- a/wpimath/src/test/native/cpp/estimator/DifferentialDrivePoseEstimatorTest.cpp
+++ b/wpimath/src/test/native/cpp/estimator/DifferentialDrivePoseEstimatorTest.cpp
@@ -293,3 +293,62 @@ TEST(DifferentialDrivePoseEstimatorTest, TestDiscardStaleVisionMeasurements) {
estimator.GetEstimatedPosition().Rotation().Radians().value(),
1e-6);
}
+
+TEST(DifferentialDrivePoseEstimatorTest, TestSampleAt) {
+ frc::DifferentialDriveKinematics kinematics{1_m};
+ frc::DifferentialDrivePoseEstimator estimator{
+ kinematics, frc::Rotation2d{}, 0_m, 0_m, frc::Pose2d{},
+ {1.0, 1.0, 1.0}, {1.0, 1.0, 1.0}};
+
+ // Returns empty when null
+ EXPECT_EQ(std::nullopt, estimator.SampleAt(1_s));
+
+ // Add odometry measurements, but don't fill up the buffer
+ // Add a tiny tolerance for the upper bound because of floating point rounding
+ // error
+ for (double time = 1; time <= 2 + 1e-9; time += 0.02) {
+ estimator.UpdateWithTime(units::second_t{time}, frc::Rotation2d{},
+ units::meter_t{time}, units::meter_t{time});
+ }
+
+ // Sample at an added time
+ EXPECT_EQ(std::optional(frc::Pose2d{1.02_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.02_s));
+ // Sample between updates (test interpolation)
+ EXPECT_EQ(std::optional(frc::Pose2d{1.01_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.01_s));
+ // Sampling before the oldest value returns the oldest value
+ EXPECT_EQ(std::optional(frc::Pose2d{1_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(0.5_s));
+ // Sampling after the newest value returns the newest value
+ EXPECT_EQ(std::optional(frc::Pose2d{2_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(2.5_s));
+
+ // Add a vision measurement after the odometry measurements (while keeping all
+ // of the old odometry measurements)
+ estimator.AddVisionMeasurement(frc::Pose2d{2_m, 0_m, frc::Rotation2d{1_rad}},
+ 2.2_s);
+
+ // Make sure nothing changed (except the newest value)
+ EXPECT_EQ(std::optional(frc::Pose2d{1.02_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.02_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{1.01_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.01_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{1_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(0.5_s));
+
+ // Add a vision measurement before the odometry measurements that's still in
+ // the buffer
+ estimator.AddVisionMeasurement(frc::Pose2d{1_m, 0.2_m, frc::Rotation2d{}},
+ 0.9_s);
+
+ // Everything should be the same except Y is 0.1 (halfway between 0 and 0.2)
+ EXPECT_EQ(std::optional(frc::Pose2d{1.02_m, 0.1_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.02_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{1.01_m, 0.1_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.01_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{1_m, 0.1_m, frc::Rotation2d{}}),
+ estimator.SampleAt(0.5_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{2_m, 0.1_m, frc::Rotation2d{}}),
+ estimator.SampleAt(2.5_s));
+}
diff --git a/wpimath/src/test/native/cpp/estimator/MecanumDrivePoseEstimatorTest.cpp b/wpimath/src/test/native/cpp/estimator/MecanumDrivePoseEstimatorTest.cpp
index 8b3a586892d..13d5c3180e5 100644
--- a/wpimath/src/test/native/cpp/estimator/MecanumDrivePoseEstimatorTest.cpp
+++ b/wpimath/src/test/native/cpp/estimator/MecanumDrivePoseEstimatorTest.cpp
@@ -295,3 +295,67 @@ TEST(MecanumDrivePoseEstimatorTest, TestDiscardStaleVisionMeasurements) {
estimator.GetEstimatedPosition().Rotation().Radians().value(),
1e-6);
}
+
+TEST(MecanumDrivePoseEstimatorTest, TestSampleAt) {
+ frc::MecanumDriveKinematics kinematics{
+ frc::Translation2d{1_m, 1_m}, frc::Translation2d{1_m, -1_m},
+ frc::Translation2d{-1_m, -1_m}, frc::Translation2d{-1_m, 1_m}};
+ frc::MecanumDrivePoseEstimator estimator{
+ kinematics, frc::Rotation2d{}, frc::MecanumDriveWheelPositions{},
+ frc::Pose2d{}, {1.0, 1.0, 1.0}, {1.0, 1.0, 1.0}};
+
+ // Returns empty when null
+ EXPECT_EQ(std::nullopt, estimator.SampleAt(1_s));
+
+ // Add odometry measurements, but don't fill up the buffer
+ // Add a tiny tolerance for the upper bound because of floating point rounding
+ // error
+ for (double time = 1; time <= 2 + 1e-9; time += 0.02) {
+ frc::MecanumDriveWheelPositions wheelPositions{
+ units::meter_t{time}, units::meter_t{time}, units::meter_t{time},
+ units::meter_t{time}};
+ estimator.UpdateWithTime(units::second_t{time}, frc::Rotation2d{},
+ wheelPositions);
+ }
+
+ // Sample at an added time
+ EXPECT_EQ(std::optional(frc::Pose2d{1.02_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.02_s));
+ // Sample between updates (test interpolation)
+ EXPECT_EQ(std::optional(frc::Pose2d{1.01_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.01_s));
+ // Sampling before the oldest value returns the oldest value
+ EXPECT_EQ(std::optional(frc::Pose2d{1_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(0.5_s));
+ // Sampling after the newest value returns the newest value
+ EXPECT_EQ(std::optional(frc::Pose2d{2_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(2.5_s));
+
+ // Add a vision measurement after the odometry measurements (while keeping all
+ // of the old odometry measurements)
+ estimator.AddVisionMeasurement(frc::Pose2d{2_m, 0_m, frc::Rotation2d{1_rad}},
+ 2.2_s);
+
+ // Make sure nothing changed (except the newest value)
+ EXPECT_EQ(std::optional(frc::Pose2d{1.02_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.02_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{1.01_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.01_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{1_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(0.5_s));
+
+ // Add a vision measurement before the odometry measurements that's still in
+ // the buffer
+ estimator.AddVisionMeasurement(frc::Pose2d{1_m, 0.2_m, frc::Rotation2d{}},
+ 0.9_s);
+
+ // Everything should be the same except Y is 0.1 (halfway between 0 and 0.2)
+ EXPECT_EQ(std::optional(frc::Pose2d{1.02_m, 0.1_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.02_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{1.01_m, 0.1_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.01_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{1_m, 0.1_m, frc::Rotation2d{}}),
+ estimator.SampleAt(0.5_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{2_m, 0.1_m, frc::Rotation2d{}}),
+ estimator.SampleAt(2.5_s));
+}
diff --git a/wpimath/src/test/native/cpp/estimator/SwerveDrivePoseEstimatorTest.cpp b/wpimath/src/test/native/cpp/estimator/SwerveDrivePoseEstimatorTest.cpp
index f1024efbc92..8a38279beae 100644
--- a/wpimath/src/test/native/cpp/estimator/SwerveDrivePoseEstimatorTest.cpp
+++ b/wpimath/src/test/native/cpp/estimator/SwerveDrivePoseEstimatorTest.cpp
@@ -311,3 +311,74 @@ TEST(SwerveDrivePoseEstimatorTest, TestDiscardStaleVisionMeasurements) {
estimator.GetEstimatedPosition().Rotation().Radians().value(),
1e-6);
}
+
+TEST(SwerveDrivePoseEstimatorTest, TestSampleAt) {
+ frc::SwerveDriveKinematics<4> kinematics{
+ frc::Translation2d{1_m, 1_m}, frc::Translation2d{1_m, -1_m},
+ frc::Translation2d{-1_m, -1_m}, frc::Translation2d{-1_m, 1_m}};
+ frc::SwerveDrivePoseEstimator estimator{
+ kinematics,
+ frc::Rotation2d{},
+ {frc::SwerveModulePosition{}, frc::SwerveModulePosition{},
+ frc::SwerveModulePosition{}, frc::SwerveModulePosition{}},
+ frc::Pose2d{},
+ {1.0, 1.0, 1.0},
+ {1.0, 1.0, 1.0}};
+
+ // Returns empty when null
+ EXPECT_EQ(std::nullopt, estimator.SampleAt(1_s));
+
+ // Add odometry measurements, but don't fill up the buffer
+ // Add a tiny tolerance for the upper bound because of floating point rounding
+ // error
+ for (double time = 1; time <= 2 + 1e-9; time += 0.02) {
+ wpi::array wheelPositions{
+ {frc::SwerveModulePosition{units::meter_t{time}, frc::Rotation2d{}},
+ frc::SwerveModulePosition{units::meter_t{time}, frc::Rotation2d{}},
+ frc::SwerveModulePosition{units::meter_t{time}, frc::Rotation2d{}},
+ frc::SwerveModulePosition{units::meter_t{time}, frc::Rotation2d{}}}};
+ estimator.UpdateWithTime(units::second_t{time}, frc::Rotation2d{},
+ wheelPositions);
+ }
+
+ // Sample at an added time
+ EXPECT_EQ(std::optional(frc::Pose2d{1.02_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.02_s));
+ // Sample between updates (test interpolation)
+ EXPECT_EQ(std::optional(frc::Pose2d{1.01_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.01_s));
+ // Sampling before the oldest value returns the oldest value
+ EXPECT_EQ(std::optional(frc::Pose2d{1_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(0.5_s));
+ // Sampling after the newest value returns the newest value
+ EXPECT_EQ(std::optional(frc::Pose2d{2_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(2.5_s));
+
+ // Add a vision measurement after the odometry measurements (while keeping all
+ // of the old odometry measurements)
+ estimator.AddVisionMeasurement(frc::Pose2d{2_m, 0_m, frc::Rotation2d{1_rad}},
+ 2.2_s);
+
+ // Make sure nothing changed (except the newest value)
+ EXPECT_EQ(std::optional(frc::Pose2d{1.02_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.02_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{1.01_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.01_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{1_m, 0_m, frc::Rotation2d{}}),
+ estimator.SampleAt(0.5_s));
+
+ // Add a vision measurement before the odometry measurements that's still in
+ // the buffer
+ estimator.AddVisionMeasurement(frc::Pose2d{1_m, 0.2_m, frc::Rotation2d{}},
+ 0.9_s);
+
+ // Everything should be the same except Y is 0.1 (halfway between 0 and 0.2)
+ EXPECT_EQ(std::optional(frc::Pose2d{1.02_m, 0.1_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.02_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{1.01_m, 0.1_m, frc::Rotation2d{}}),
+ estimator.SampleAt(1.01_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{1_m, 0.1_m, frc::Rotation2d{}}),
+ estimator.SampleAt(0.5_s));
+ EXPECT_EQ(std::optional(frc::Pose2d{2_m, 0.1_m, frc::Rotation2d{}}),
+ estimator.SampleAt(2.5_s));
+}
diff --git a/wpiunits/src/main/java/edu/wpi/first/units/Units.java b/wpiunits/src/main/java/edu/wpi/first/units/Units.java
index 825acf4b5b4..26b6b358cbe 100644
--- a/wpiunits/src/main/java/edu/wpi/first/units/Units.java
+++ b/wpiunits/src/main/java/edu/wpi/first/units/Units.java
@@ -182,6 +182,13 @@ private Units() {
public static final Velocity> MetersPerSecondPerSecond =
MetersPerSecond.per(Second);
+ /**
+ * A unit of linear acceleration equivalent to accelerating at a rate of one {@link #Foot Foot}
+ * per {@link #Second} every second.
+ */
+ public static final Velocity> FeetPerSecondPerSecond =
+ FeetPerSecond.per(Second);
+
/**
* A unit of angular acceleration equivalent to accelerating at a rate of one {@link #Rotations
* Rotation} per {@link #Second} every second.