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}