From 21b5389bbefa08e50803cdf6b206d5440839c818 Mon Sep 17 00:00:00 2001 From: Michael Lesirge <100492377+MichaelLesirge@users.noreply.github.com> Date: Wed, 14 Jan 2026 20:22:07 -0800 Subject: [PATCH] [wpimath,cmd] Add multi tap boolean stream filter and multi tap trigger modifier (double tap detector) (#8307) Add a simple tap counting filter for boolean streams. The filter activates when the input has risen (transitioned from false to true, like when a button is tapped) the required number of times within the time window after the first rising edge. Once activated, the output remains true as long as the input is true. The tap count resets when the time window expires or when the input goes false after activation. Example usage: ```java xbox.a() .multiPress(2, 0.2) // Detect a double tap within 0.2 seconds .onTrue(Commands.print("Double tapped A button")); xbox.y() .multiPress(2, 0.5) // Detect a double tap within 0.5 seconds .whileTrue(Commands.print("Y held after tap").repeatedly()); ``` This is not a noise reduction and/or input smoothing filter, but it is similar in usage to debounce, so I believe it could be considered a filter, but am open to a better location. I believe this would be a useful addition, as double/triple tapping a button is a common control option in games, yet is not often utilized by newer FRC teams. I believe adding it to WPILib in a standard way will allow more teams to make the most out of their controls. --- .../wpilibj2/command/button/Trigger.java | 28 ++++ .../cpp/frc2/command/button/Trigger.cpp | 9 ++ .../include/frc2/command/button/Trigger.h | 16 +++ .../first/math/filter/EdgeCounterFilter.java | 120 ++++++++++++++++++ .../native/cpp/filter/EdgeCountFilter.cpp | 46 +++++++ .../include/frc/filter/EdgeCountFilter.h | 94 ++++++++++++++ .../math/filter/EdgeCounterFilterTest.java | 82 ++++++++++++ .../native/cpp/filter/EdgeCountFilterTest.cpp | 75 +++++++++++ 8 files changed, 470 insertions(+) create mode 100644 wpimath/src/main/java/edu/wpi/first/math/filter/EdgeCounterFilter.java create mode 100644 wpimath/src/main/native/cpp/filter/EdgeCountFilter.cpp create mode 100644 wpimath/src/main/native/include/frc/filter/EdgeCountFilter.h create mode 100644 wpimath/src/test/java/edu/wpi/first/math/filter/EdgeCounterFilterTest.java create mode 100644 wpimath/src/test/native/cpp/filter/EdgeCountFilterTest.cpp diff --git a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/button/Trigger.java b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/button/Trigger.java index 65c6145f6b..fc8b3b1bbe 100644 --- a/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/button/Trigger.java +++ b/wpilibNewCommands/src/main/java/edu/wpi/first/wpilibj2/command/button/Trigger.java @@ -7,6 +7,7 @@ package edu.wpi.first.wpilibj2.command.button; import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; import edu.wpi.first.math.filter.Debouncer; +import edu.wpi.first.math.filter.EdgeCounterFilter; import edu.wpi.first.wpilibj.event.EventLoop; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.CommandScheduler; @@ -287,4 +288,31 @@ public class Trigger implements BooleanSupplier { } }); } + + /** + * Creates a new multi-press trigger from this trigger - it will become active when this trigger + * has been activated the required number of times within the specified time window. + * + *
This is useful for implementing "double-click" style functionality. + * + *
Input for this must be stable, consider using a Debouncer before this to avoid counting
+ * noise as multiple presses.
+ *
+ * @param requiredPresses The number of presses required.
+ * @param windowTime The number of seconds in which the presses must occur.
+ * @return The multi-press trigger.
+ */
+ public Trigger multiPress(int requiredPresses, double windowTime) {
+ return new Trigger(
+ m_loop,
+ new BooleanSupplier() {
+ final EdgeCounterFilter m_edgeCounterFilter =
+ new EdgeCounterFilter(requiredPresses, windowTime);
+
+ @Override
+ public boolean getAsBoolean() {
+ return m_edgeCounterFilter.calculate(m_condition.getAsBoolean());
+ }
+ });
+ }
}
diff --git a/wpilibNewCommands/src/main/native/cpp/frc2/command/button/Trigger.cpp b/wpilibNewCommands/src/main/native/cpp/frc2/command/button/Trigger.cpp
index 6afdbb2b2c..6716fe6bc2 100644
--- a/wpilibNewCommands/src/main/native/cpp/frc2/command/button/Trigger.cpp
+++ b/wpilibNewCommands/src/main/native/cpp/frc2/command/button/Trigger.cpp
@@ -7,6 +7,7 @@
#include This is useful for implementing "double-click" style functionality.
+ *
+ * Input for this must be stable, consider using a Debouncer before this to
+ * avoid counting noise as multiple presses.
+ *
+ * @param requiredPresses The number of presses required.
+ * @param windowTime The time in which the presses must occur.
+ * @return The multi-press trigger.
+ */
+ Trigger MultiPress(int requiredPresses, units::second_t windowTime);
+
/**
* Returns the current state of this trigger.
*
diff --git a/wpimath/src/main/java/edu/wpi/first/math/filter/EdgeCounterFilter.java b/wpimath/src/main/java/edu/wpi/first/math/filter/EdgeCounterFilter.java
new file mode 100644
index 0000000000..7a4d80e296
--- /dev/null
+++ b/wpimath/src/main/java/edu/wpi/first/math/filter/EdgeCounterFilter.java
@@ -0,0 +1,120 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+package edu.wpi.first.math.filter;
+
+import edu.wpi.first.math.MathSharedStore;
+
+/**
+ * A rising edge counter for boolean streams. Requires that the boolean change value to true for a
+ * specified number of times within a specified time window after the first rising edge before the
+ * filtered value changes.
+ *
+ * The filter activates when the input has risen (transitioned from false to true) the required
+ * number of times within the time window. Once activated, the output remains true as long as the
+ * input is true. The edge count resets when the time window expires or when the input goes false
+ * after activation.
+ *
+ * Input must be stable; consider using a Debouncer before this filter to avoid counting noise as
+ * multiple edges.
+ */
+public class EdgeCounterFilter {
+ private int m_requiredEdges;
+ private double m_windowTimeSeconds;
+
+ private double m_firstEdgeTimeSeconds;
+ private int m_currentCount;
+
+ private boolean m_lastInput;
+
+ /**
+ * Creates a new EdgeCounterFilter.
+ *
+ * @param requiredEdges The number of rising edges required before the output goes true.
+ * @param windowTime The maximum number of seconds in which all required edges must occur after
+ * the first rising edge.
+ */
+ public EdgeCounterFilter(int requiredEdges, double windowTime) {
+ m_requiredEdges = requiredEdges;
+ m_windowTimeSeconds = windowTime;
+
+ resetTimer();
+ }
+
+ private void resetTimer() {
+ m_firstEdgeTimeSeconds = MathSharedStore.getTimestamp();
+ }
+
+ private boolean hasElapsed() {
+ return MathSharedStore.getTimestamp() - m_firstEdgeTimeSeconds >= m_windowTimeSeconds;
+ }
+
+ /**
+ * Applies the edge counter filter to the input stream.
+ *
+ * @param input The current value of the input stream.
+ * @return True if the required number of edges have occurred within the time window and the input
+ * is currently true; false otherwise.
+ */
+ public boolean calculate(boolean input) {
+ boolean enoughEdges = m_currentCount >= m_requiredEdges;
+
+ boolean expired = hasElapsed() && !enoughEdges;
+ boolean activationEnded = !input && enoughEdges;
+
+ if (expired || activationEnded) {
+ m_currentCount = 0;
+ }
+
+ if (input && !m_lastInput) {
+ if (m_currentCount == 0) {
+ resetTimer(); // Start timer on first rising edge
+ }
+
+ m_currentCount++;
+ }
+
+ m_lastInput = input;
+
+ return input && m_currentCount >= m_requiredEdges;
+ }
+
+ /**
+ * Sets the time window duration.
+ *
+ * @param windowTime The maximum window of seconds in which all required edges must occur after
+ * the first rising edge.
+ */
+ public void setWindowTime(double windowTime) {
+ m_windowTimeSeconds = windowTime;
+ }
+
+ /**
+ * Gets the time window duration.
+ *
+ * @return The maximum window of seconds in which all required edges must occur after the first
+ * rising edge.
+ */
+ public double getWindowTime() {
+ return m_windowTimeSeconds;
+ }
+
+ /**
+ * Sets the required number of edges.
+ *
+ * @param requiredEdges The number of rising edges required before the output goes true.
+ */
+ public void setRequiredEdges(int requiredEdges) {
+ m_requiredEdges = requiredEdges;
+ }
+
+ /**
+ * Gets the required number of edges.
+ *
+ * @return The number of rising edges required before the output goes true.
+ */
+ public int getRequiredEdges() {
+ return m_requiredEdges;
+ }
+}
diff --git a/wpimath/src/main/native/cpp/filter/EdgeCountFilter.cpp b/wpimath/src/main/native/cpp/filter/EdgeCountFilter.cpp
new file mode 100644
index 0000000000..dfa5e47ff4
--- /dev/null
+++ b/wpimath/src/main/native/cpp/filter/EdgeCountFilter.cpp
@@ -0,0 +1,46 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#include "frc/filter/EdgeCountFilter.h"
+
+#include "wpimath/MathShared.h"
+
+using namespace frc;
+
+EdgeCounterFilter::EdgeCounterFilter(int requiredEdges, units::second_t window)
+ : m_requiredEdges(requiredEdges), m_windowTime(window) {
+ ResetTimer();
+}
+
+void EdgeCounterFilter::ResetTimer() {
+ m_firstEdgeTime = wpi::math::MathSharedStore::GetTimestamp();
+}
+
+bool EdgeCounterFilter::HasElapsed() const {
+ return wpi::math::MathSharedStore::GetTimestamp() - m_firstEdgeTime >=
+ m_windowTime;
+}
+
+bool EdgeCounterFilter::Calculate(bool input) {
+ bool enoughEdges = m_currentCount >= m_requiredEdges;
+
+ bool expired = HasElapsed() && !enoughEdges;
+ bool activationEnded = !input && enoughEdges;
+
+ if (expired || activationEnded) {
+ m_currentCount = 0;
+ }
+
+ if (input && !m_lastInput) {
+ if (m_currentCount == 0) {
+ ResetTimer(); // Start timer on first rising edge
+ }
+
+ m_currentCount++;
+ }
+
+ m_lastInput = input;
+
+ return input && m_currentCount >= m_requiredEdges;
+}
diff --git a/wpimath/src/main/native/include/frc/filter/EdgeCountFilter.h b/wpimath/src/main/native/include/frc/filter/EdgeCountFilter.h
new file mode 100644
index 0000000000..3fb682d24c
--- /dev/null
+++ b/wpimath/src/main/native/include/frc/filter/EdgeCountFilter.h
@@ -0,0 +1,94 @@
+// Copyright (c) FIRST and other WPILib contributors.
+// Open Source Software; you can modify and/or share it under the terms of
+// the WPILib BSD license file in the root directory of this project.
+
+#pragma once
+
+#include