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 org.wpilib.command3;
006
007import static org.wpilib.units.Units.Seconds;
008import static org.wpilib.util.ErrorMessages.requireNonNullParam;
009
010import java.util.Arrays;
011import java.util.Collection;
012import java.util.List;
013import java.util.function.BooleanSupplier;
014import java.util.function.Consumer;
015import org.wpilib.system.Timer;
016import org.wpilib.units.measure.Time;
017
018/**
019 * A coroutine object is injected into command's {@link Command#run(Coroutine)} method to allow
020 * commands to yield and compositions to run other commands. Commands are considered <i>bound</i> to
021 * a coroutine while they're scheduled; attempting to use a coroutine outside the command bound to
022 * it will result in an {@code IllegalStateException} being thrown.
023 */
024public final class Coroutine {
025  private final Scheduler m_scheduler;
026  private final Continuation m_backingContinuation;
027
028  /**
029   * Creates a new coroutine. Package-private; only the scheduler should be creating these.
030   *
031   * @param scheduler The scheduler running the coroutine
032   * @param scope The continuation scope the coroutine's backing continuation runs in
033   * @param callback The callback for the continuation to execute when mounted. Often a command
034   *     function's body.
035   */
036  Coroutine(Scheduler scheduler, ContinuationScope scope, Consumer<Coroutine> callback) {
037    m_scheduler = scheduler;
038    m_backingContinuation = new Continuation(scope, () -> callback.accept(this));
039  }
040
041  /**
042   * Yields control back to the scheduler to allow other commands to execute. This can be thought of
043   * as "pausing" the currently executing command.
044   *
045   * @return true
046   * @throws IllegalStateException if called anywhere other than the coroutine's running command
047   */
048  public boolean yield() {
049    requireMounted();
050
051    return m_backingContinuation.yield();
052  }
053
054  /**
055   * Parks the current command. No code in a command declared after calling {@code park()} will be
056   * executed. A parked command will never complete naturally and must be interrupted or canceled.
057   *
058   * @throws IllegalStateException if called anywhere other than the coroutine's running command
059   */
060  @SuppressWarnings("InfiniteLoopStatement")
061  public void park() {
062    requireMounted();
063
064    while (true) {
065      // 'this' is required because 'yield' is a semi-keyword and needs to be qualified
066      this.yield();
067    }
068  }
069
070  /**
071   * Schedules a child command and then immediately returns. The child command will run until its
072   * natural completion, the parent command exits, or the parent command cancels it.
073   *
074   * <p>This is a nonblocking operation. To fork and then wait for the child command to complete,
075   * use {@link #await(Command)}.
076   *
077   * <p>The parent command will continue executing while the child command runs, and can resync with
078   * the child command using {@link #await(Command)}.
079   *
080   * <pre>{@code
081   * Command example() {
082   *   return Command.noRequirements(coroutine -> {
083   *     Command child = ...;
084   *     coroutine.fork(child);
085   *     // ... do more things
086   *     // then sync back up with the child command
087   *     coroutine.await(child);
088   *   }).named("Example");
089   * }
090   * }</pre>
091   *
092   * <p>Note: forking a command that conflicts with a higher-priority command will fail. The forked
093   * command will not be scheduled, and the existing command will continue to run.
094   *
095   * @param commands The commands to fork.
096   * @throws IllegalStateException if called anywhere other than the coroutine's running command
097   * @see #await(Command)
098   */
099  public void fork(Command... commands) {
100    requireMounted();
101
102    requireNonNullParam(commands, "commands", "Coroutine.fork");
103    for (int i = 0; i < commands.length; i++) {
104      requireNonNullParam(commands[i], "commands[" + i + "]", "Coroutine.fork");
105    }
106
107    // Check for user error; there's no reason to fork conflicting commands simultaneously
108    ConflictDetector.throwIfConflicts(List.of(commands));
109
110    // Shorthand; this is handy for user-defined compositions
111    for (var command : commands) {
112      m_scheduler.schedule(command);
113    }
114  }
115
116  /**
117   * Forks off some commands. Each command will run until its natural completion, the parent command
118   * exits, or the parent command cancels it. The parent command will continue executing while the
119   * forked commands run, and can resync with the forked commands using {@link
120   * #awaitAll(Collection)}.
121   *
122   * <pre>{@code
123   * Command example() {
124   *   return Command.noRequirements(coroutine -> {
125   *     Collection<Command> innerCommands = ...;
126   *     coroutine.fork(innerCommands);
127   *     // ... do more things
128   *     // then sync back up with the inner commands
129   *     coroutine.awaitAll(innerCommands);
130   *   }).named("Example");
131   * }
132   * }</pre>
133   *
134   * <p>Note: forking a command that conflicts with a higher-priority command will fail. The forked
135   * command will not be scheduled, and the existing command will continue to run.
136   *
137   * @param commands The commands to fork.
138   * @throws IllegalStateException if called anywhere other than the coroutine's running command
139   */
140  public void fork(Collection<? extends Command> commands) {
141    fork(commands.toArray(Command[]::new));
142  }
143
144  /**
145   * Awaits completion of a command. If the command is not currently scheduled or running, it will
146   * be scheduled automatically. This is a blocking operation and will not return until the command
147   * completes or has been interrupted by another command scheduled by the same parent.
148   *
149   * @param command the command to await
150   * @throws IllegalStateException if called anywhere other than the coroutine's running command
151   * @see #fork(Command...)
152   */
153  public void await(Command command) {
154    requireMounted();
155
156    requireNonNullParam(command, "command", "Coroutine.await");
157
158    m_scheduler.schedule(command);
159
160    while (m_scheduler.isScheduledOrRunning(command)) {
161      // If the command is a one-shot, then the schedule call will completely execute the command.
162      // There would be nothing to await
163      this.yield();
164    }
165  }
166
167  /**
168   * Awaits completion of all given commands. If any command is not current scheduled or running, it
169   * will be scheduled.
170   *
171   * @param commands the commands to await
172   * @throws IllegalArgumentException if any of the commands conflict with each other
173   * @throws IllegalStateException if called anywhere other than the coroutine's running command
174   */
175  public void awaitAll(Collection<? extends Command> commands) {
176    requireMounted();
177
178    requireNonNullParam(commands, "commands", "Coroutine.awaitAll");
179    int i = 0;
180    for (Command command : commands) {
181      requireNonNullParam(command, "commands[" + i + "]", "Coroutine.awaitAll");
182      i++;
183    }
184
185    ConflictDetector.throwIfConflicts(commands);
186
187    for (var command : commands) {
188      m_scheduler.schedule(command);
189    }
190
191    while (commands.stream().anyMatch(m_scheduler::isScheduledOrRunning)) {
192      this.yield();
193    }
194  }
195
196  /**
197   * Awaits completion of all given commands. If any command is not current scheduled or running, it
198   * will be scheduled.
199   *
200   * @param commands the commands to await
201   * @throws IllegalArgumentException if any of the commands conflict with each other
202   * @throws IllegalStateException if called anywhere other than the coroutine's running command
203   */
204  public void awaitAll(Command... commands) {
205    awaitAll(Arrays.asList(commands));
206  }
207
208  /**
209   * Awaits completion of any given commands. Any command that's not already scheduled or running
210   * will be scheduled. After any of the given commands completes, the rest will be canceled.
211   *
212   * @param commands the commands to await
213   * @throws IllegalArgumentException if any of the commands conflict with each other
214   * @throws IllegalStateException if called anywhere other than the coroutine's running command
215   */
216  public void awaitAny(Collection<? extends Command> commands) {
217    requireMounted();
218
219    requireNonNullParam(commands, "commands", "Coroutine.awaitAny");
220    int i = 0;
221    for (Command command : commands) {
222      requireNonNullParam(command, "commands[" + i + "]", "Coroutine.awaitAny");
223      i++;
224    }
225
226    ConflictDetector.throwIfConflicts(commands);
227
228    // Schedule anything that's not already queued or running
229    for (var command : commands) {
230      m_scheduler.schedule(command);
231    }
232
233    while (commands.stream().allMatch(m_scheduler::isScheduledOrRunning)) {
234      this.yield();
235    }
236
237    // At least one command exited; cancel the rest.
238    commands.forEach(m_scheduler::cancel);
239  }
240
241  /**
242   * Awaits completion of any given commands. Any command that's not already scheduled or running
243   * will be scheduled. After any of the given commands completes, the rest will be canceled.
244   *
245   * @param commands the commands to await
246   * @throws IllegalArgumentException if any of the commands conflict with each other
247   * @throws IllegalStateException if called anywhere other than the coroutine's running command
248   */
249  public void awaitAny(Command... commands) {
250    awaitAny(Arrays.asList(commands));
251  }
252
253  /**
254   * Waits for some duration of time to elapse. Returns immediately if the given duration is zero or
255   * negative. Call this within a command or command composition to introduce a simple delay.
256   *
257   * <p>For example, a basic autonomous routine that drives straight for 5 seconds:
258   *
259   * <pre>{@code
260   * Command timedDrive() {
261   *   return drivebase.run(coroutine -> {
262   *     drivebase.tankDrive(1, 1);
263   *     coroutine.wait(Seconds.of(5));
264   *     drivebase.stop();
265   *   }).named("Timed Drive");
266   * }
267   * }</pre>
268   *
269   * <p>Note that the resolution of the wait period is equal to the period at which {@link
270   * Scheduler#run()} is called by the robot program. If using a 20 millisecond update period, the
271   * wait will be rounded up to the nearest 20 millisecond interval; in this scenario, calling
272   * {@code wait(Milliseconds.of(1))} and {@code wait(Milliseconds.of(19))} would have identical
273   * effects.
274   *
275   * <p>Very small loop times near the loop period are sensitive to the order in which commands are
276   * executed. If a command waits for 10 ms at the end of a scheduler cycle, and then all the
277   * commands that ran before it complete or exit, and then the next cycle starts immediately, the
278   * wait will be evaluated at the <i>start</i> of that next cycle, which occurred less than 10 ms
279   * later. Therefore, the wait will see not enough time has passed and only exit after an
280   * additional cycle elapses, adding an unexpected extra 20 ms to the wait time. This becomes less
281   * of a problem with smaller loop periods, as the extra 1-loop delay becomes smaller.
282   *
283   * @param duration the duration of time to wait
284   * @throws IllegalStateException if called anywhere other than the coroutine's running command
285   */
286  public void wait(Time duration) {
287    requireMounted();
288
289    requireNonNullParam(duration, "duration", "Coroutine.wait");
290
291    var timer = new Timer();
292    timer.start();
293    while (!timer.hasElapsed(duration.in(Seconds))) {
294      this.yield();
295    }
296  }
297
298  /**
299   * Yields until a condition is met.
300   *
301   * @param condition The condition to wait for
302   * @throws IllegalStateException if called anywhere other than the coroutine's running command
303   */
304  public void waitUntil(BooleanSupplier condition) {
305    requireMounted();
306
307    requireNonNullParam(condition, "condition", "Coroutine.waitUntil");
308
309    while (!condition.getAsBoolean()) {
310      this.yield();
311    }
312  }
313
314  /**
315   * Advanced users only: this permits access to the backing command scheduler to run custom logic
316   * not provided by the standard coroutine methods. Any commands manually scheduled through this
317   * will be canceled when the parent command exits - it's not possible to "fork" a command that
318   * lives longer than the command that scheduled it.
319   *
320   * @return the command scheduler backing this coroutine
321   * @throws IllegalStateException if called anywhere other than the coroutine's running command
322   */
323  public Scheduler scheduler() {
324    requireMounted();
325
326    return m_scheduler;
327  }
328
329  private boolean isMounted() {
330    return m_backingContinuation.isMounted();
331  }
332
333  private void requireMounted() {
334    // Note: attempting to yield() outside a command will already throw an error due to the
335    // continuation being unmounted, but other actions like forking and awaiting should also
336    // throw errors. For consistent messaging, we use this helper in all places, not just the
337    // ones that interact with the backing continuation.
338
339    if (isMounted()) {
340      return;
341    }
342
343    throw new IllegalStateException("Coroutines can only be used by the command bound to them");
344  }
345
346  // Package-private for interaction with the scheduler.
347  // These functions are not intended for team use.
348
349  void runToYieldPoint() {
350    m_backingContinuation.run();
351  }
352
353  void mount() {
354    Continuation.mountContinuation(m_backingContinuation);
355  }
356
357  boolean isDone() {
358    return m_backingContinuation.isDone();
359  }
360}