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