001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.wpilibj2.command.button; 006 007import static edu.wpi.first.util.ErrorMessages.requireNonNullParam; 008 009import edu.wpi.first.math.filter.Debouncer; 010import edu.wpi.first.math.filter.EdgeCounterFilter; 011import edu.wpi.first.wpilibj.event.EventLoop; 012import edu.wpi.first.wpilibj2.command.Command; 013import edu.wpi.first.wpilibj2.command.CommandScheduler; 014import java.util.function.BooleanSupplier; 015 016/** 017 * This class provides an easy way to link commands to conditions. 018 * 019 * <p>It is very easy to link a button to a command. For instance, you could link the trigger button 020 * of a joystick to a "score" command. 021 * 022 * <p>Triggers can easily be composed for advanced functionality using the {@link 023 * #and(BooleanSupplier)}, {@link #or(BooleanSupplier)}, {@link #negate()} operators. 024 * 025 * <p>This class is provided by the NewCommands VendorDep 026 */ 027public class Trigger implements BooleanSupplier { 028 /** Functional interface for the body of a trigger binding. */ 029 @FunctionalInterface 030 private interface BindingBody { 031 /** 032 * Executes the body of the binding. 033 * 034 * @param previous The previous state of the condition. 035 * @param current The current state of the condition. 036 */ 037 void run(boolean previous, boolean current); 038 } 039 040 private final BooleanSupplier m_condition; 041 private final EventLoop m_loop; 042 043 /** 044 * Creates a new trigger based on the given condition. 045 * 046 * @param loop The loop instance that polls this trigger. 047 * @param condition the condition represented by this trigger 048 */ 049 public Trigger(EventLoop loop, BooleanSupplier condition) { 050 m_loop = requireNonNullParam(loop, "loop", "Trigger"); 051 m_condition = requireNonNullParam(condition, "condition", "Trigger"); 052 } 053 054 /** 055 * Creates a new trigger based on the given condition. 056 * 057 * <p>Polled by the default scheduler button loop. 058 * 059 * @param condition the condition represented by this trigger 060 */ 061 public Trigger(BooleanSupplier condition) { 062 this(CommandScheduler.getInstance().getDefaultButtonLoop(), condition); 063 } 064 065 /** 066 * Adds a binding to the EventLoop. 067 * 068 * @param body The body of the binding to add. 069 */ 070 private void addBinding(BindingBody body) { 071 m_loop.bind( 072 new Runnable() { 073 private boolean m_previous = m_condition.getAsBoolean(); 074 075 @Override 076 public void run() { 077 boolean current = m_condition.getAsBoolean(); 078 079 body.run(m_previous, current); 080 081 m_previous = current; 082 } 083 }); 084 } 085 086 /** 087 * Starts the command when the condition changes. 088 * 089 * @param command the command to start 090 * @return this trigger, so calls can be chained 091 */ 092 public Trigger onChange(Command command) { 093 requireNonNullParam(command, "command", "onChange"); 094 addBinding( 095 (previous, current) -> { 096 if (previous != current) { 097 CommandScheduler.getInstance().schedule(command); 098 } 099 }); 100 return this; 101 } 102 103 /** 104 * Starts the given command whenever the condition changes from `false` to `true`. 105 * 106 * @param command the command to start 107 * @return this trigger, so calls can be chained 108 */ 109 public Trigger onTrue(Command command) { 110 requireNonNullParam(command, "command", "onTrue"); 111 addBinding( 112 (previous, current) -> { 113 if (!previous && current) { 114 CommandScheduler.getInstance().schedule(command); 115 } 116 }); 117 return this; 118 } 119 120 /** 121 * Starts the given command whenever the condition changes from `true` to `false`. 122 * 123 * @param command the command to start 124 * @return this trigger, so calls can be chained 125 */ 126 public Trigger onFalse(Command command) { 127 requireNonNullParam(command, "command", "onFalse"); 128 addBinding( 129 (previous, current) -> { 130 if (previous && !current) { 131 CommandScheduler.getInstance().schedule(command); 132 } 133 }); 134 return this; 135 } 136 137 /** 138 * Starts the given command when the condition changes to `true` and cancels it when the condition 139 * changes to `false`. 140 * 141 * <p>Doesn't re-start the command if it ends while the condition is still `true`. If the command 142 * should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}. 143 * 144 * @param command the command to start 145 * @return this trigger, so calls can be chained 146 */ 147 public Trigger whileTrue(Command command) { 148 requireNonNullParam(command, "command", "whileTrue"); 149 addBinding( 150 (previous, current) -> { 151 if (!previous && current) { 152 CommandScheduler.getInstance().schedule(command); 153 } else if (previous && !current) { 154 command.cancel(); 155 } 156 }); 157 return this; 158 } 159 160 /** 161 * Starts the given command when the condition changes to `false` and cancels it when the 162 * condition changes to `true`. 163 * 164 * <p>Doesn't re-start the command if it ends while the condition is still `false`. If the command 165 * should restart, see {@link edu.wpi.first.wpilibj2.command.RepeatCommand}. 166 * 167 * @param command the command to start 168 * @return this trigger, so calls can be chained 169 */ 170 public Trigger whileFalse(Command command) { 171 requireNonNullParam(command, "command", "whileFalse"); 172 addBinding( 173 (previous, current) -> { 174 if (previous && !current) { 175 CommandScheduler.getInstance().schedule(command); 176 } else if (!previous && current) { 177 command.cancel(); 178 } 179 }); 180 return this; 181 } 182 183 /** 184 * Toggles a command when the condition changes from `false` to `true`. 185 * 186 * @param command the command to toggle 187 * @return this trigger, so calls can be chained 188 */ 189 public Trigger toggleOnTrue(Command command) { 190 requireNonNullParam(command, "command", "toggleOnTrue"); 191 addBinding( 192 (previous, current) -> { 193 if (!previous && current) { 194 if (command.isScheduled()) { 195 command.cancel(); 196 } else { 197 CommandScheduler.getInstance().schedule(command); 198 } 199 } 200 }); 201 return this; 202 } 203 204 /** 205 * Toggles a command when the condition changes from `true` to `false`. 206 * 207 * @param command the command to toggle 208 * @return this trigger, so calls can be chained 209 */ 210 public Trigger toggleOnFalse(Command command) { 211 requireNonNullParam(command, "command", "toggleOnFalse"); 212 addBinding( 213 (previous, current) -> { 214 if (previous && !current) { 215 if (command.isScheduled()) { 216 command.cancel(); 217 } else { 218 CommandScheduler.getInstance().schedule(command); 219 } 220 } 221 }); 222 return this; 223 } 224 225 @Override 226 public boolean getAsBoolean() { 227 return m_condition.getAsBoolean(); 228 } 229 230 /** 231 * Composes two triggers with logical AND. 232 * 233 * @param trigger the condition to compose with 234 * @return A trigger which is active when both component triggers are active. 235 */ 236 public Trigger and(BooleanSupplier trigger) { 237 return new Trigger(m_loop, () -> m_condition.getAsBoolean() && trigger.getAsBoolean()); 238 } 239 240 /** 241 * Composes two triggers with logical OR. 242 * 243 * @param trigger the condition to compose with 244 * @return A trigger which is active when either component trigger is active. 245 */ 246 public Trigger or(BooleanSupplier trigger) { 247 return new Trigger(m_loop, () -> m_condition.getAsBoolean() || trigger.getAsBoolean()); 248 } 249 250 /** 251 * Creates a new trigger that is active when this trigger is inactive, i.e. that acts as the 252 * negation of this trigger. 253 * 254 * @return the negated trigger 255 */ 256 public Trigger negate() { 257 return new Trigger(m_loop, () -> !m_condition.getAsBoolean()); 258 } 259 260 /** 261 * Creates a new debounced trigger from this trigger - it will become active when this trigger has 262 * been active for longer than the specified period. 263 * 264 * @param seconds The debounce period. 265 * @return The debounced trigger (rising edges debounced only) 266 */ 267 public Trigger debounce(double seconds) { 268 return debounce(seconds, Debouncer.DebounceType.kRising); 269 } 270 271 /** 272 * Creates a new debounced trigger from this trigger - it will become active when this trigger has 273 * been active for longer than the specified period. 274 * 275 * @param seconds The debounce period. 276 * @param type The debounce type. 277 * @return The debounced trigger. 278 */ 279 public Trigger debounce(double seconds, Debouncer.DebounceType type) { 280 return new Trigger( 281 m_loop, 282 new BooleanSupplier() { 283 final Debouncer m_debouncer = new Debouncer(seconds, type); 284 285 @Override 286 public boolean getAsBoolean() { 287 return m_debouncer.calculate(m_condition.getAsBoolean()); 288 } 289 }); 290 } 291 292 /** 293 * Creates a new multi-press trigger from this trigger - it will become active when this trigger 294 * has been activated the required number of times within the specified time window. 295 * 296 * <p>This is useful for implementing "double-click" style functionality. 297 * 298 * <p>Input for this must be stable, consider using a Debouncer before this to avoid counting 299 * noise as multiple presses. 300 * 301 * @param requiredPresses The number of presses required. 302 * @param windowTime The number of seconds in which the presses must occur. 303 * @return The multi-press trigger. 304 */ 305 public Trigger multiPress(int requiredPresses, double windowTime) { 306 return new Trigger( 307 m_loop, 308 new BooleanSupplier() { 309 final EdgeCounterFilter m_edgeCounterFilter = 310 new EdgeCounterFilter(requiredPresses, windowTime); 311 312 @Override 313 public boolean getAsBoolean() { 314 return m_edgeCounterFilter.calculate(m_condition.getAsBoolean()); 315 } 316 }); 317 } 318}