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.wpilibj.command; 006 007import static java.util.Objects.requireNonNull; 008 009import java.util.Enumeration; 010import java.util.Vector; 011 012/** 013 * A {@link CommandGroup} is a list of commands which are executed in sequence. 014 * 015 * <p>Commands in a {@link CommandGroup} are added using the {@link 016 * CommandGroup#addSequential(Command) addSequential(...)} method and are called sequentially. 017 * {@link CommandGroup CommandGroups} are themselves {@link Command commands} and can be given to 018 * other {@link CommandGroup CommandGroups}. 019 * 020 * <p>{@link CommandGroup CommandGroups} will carry all of the requirements of their {@link Command 021 * subcommands}. Additional requirements can be specified by calling {@link 022 * CommandGroup#requires(Subsystem) requires(...)} normally in the constructor. 023 * 024 * <p>CommandGroups can also execute commands in parallel, simply by adding them using {@link 025 * CommandGroup#addParallel(Command) addParallel(...)}. 026 * 027 * @see Command 028 * @see Subsystem 029 * @see IllegalUseOfCommandException 030 */ 031public class CommandGroup extends Command { 032 /** The commands in this group (stored in entries). */ 033 @SuppressWarnings("PMD.UseArrayListInsteadOfVector") 034 private final Vector<Entry> m_commands = new Vector<>(); 035 /* 036 * Intentionally package private 037 */ 038 /** The active children in this group (stored in entries). */ 039 @SuppressWarnings("PMD.UseArrayListInsteadOfVector") 040 final Vector<Entry> m_children = new Vector<>(); 041 /** The current command, -1 signifies that none have been run. */ 042 private int m_currentCommandIndex = -1; 043 044 /** 045 * Creates a new {@link CommandGroup CommandGroup}. The name of this command will be set to its 046 * class name. 047 */ 048 public CommandGroup() {} 049 050 /** 051 * Creates a new {@link CommandGroup CommandGroup} with the given name. 052 * 053 * @param name the name for this command group 054 * @throws IllegalArgumentException if name is null 055 */ 056 public CommandGroup(String name) { 057 super(name); 058 } 059 060 /** 061 * Adds a new {@link Command Command} to the group. The {@link Command Command} will be started 062 * after all the previously added {@link Command Commands}. 063 * 064 * <p>Note that any requirements the given {@link Command Command} has will be added to the group. 065 * For this reason, a {@link Command Command's} requirements can not be changed after being added 066 * to a group. 067 * 068 * <p>It is recommended that this method be called in the constructor. 069 * 070 * @param command The {@link Command Command} to be added 071 * @throws IllegalUseOfCommandException if the group has been started before or been given to 072 * another group 073 * @throws IllegalArgumentException if command is null 074 */ 075 public final synchronized void addSequential(Command command) { 076 validate("Can not add new command to command group"); 077 if (command == null) { 078 throw new IllegalArgumentException("Given null command"); 079 } 080 081 command.setParent(this); 082 083 m_commands.addElement(new Entry(command, Entry.IN_SEQUENCE)); 084 for (Enumeration<?> e = command.getRequirements(); e.hasMoreElements(); ) { 085 requires((Subsystem) e.nextElement()); 086 } 087 } 088 089 /** 090 * Adds a new {@link Command Command} to the group with a given timeout. The {@link Command 091 * Command} will be started after all the previously added commands. 092 * 093 * <p>Once the {@link Command Command} is started, it will be run until it finishes or the time 094 * expires, whichever is sooner. Note that the given {@link Command Command} will have no 095 * knowledge that it is on a timer. 096 * 097 * <p>Note that any requirements the given {@link Command Command} has will be added to the group. 098 * For this reason, a {@link Command Command's} requirements can not be changed after being added 099 * to a group. 100 * 101 * <p>It is recommended that this method be called in the constructor. 102 * 103 * @param command The {@link Command Command} to be added 104 * @param timeout The timeout (in seconds) 105 * @throws IllegalUseOfCommandException if the group has been started before or been given to 106 * another group or if the {@link Command Command} has been started before or been given to 107 * another group 108 * @throws IllegalArgumentException if command is null or timeout is negative 109 */ 110 public final synchronized void addSequential(Command command, double timeout) { 111 validate("Can not add new command to command group"); 112 if (command == null) { 113 throw new IllegalArgumentException("Given null command"); 114 } 115 if (timeout < 0) { 116 throw new IllegalArgumentException("Can not be given a negative timeout"); 117 } 118 119 command.setParent(this); 120 121 m_commands.addElement(new Entry(command, Entry.IN_SEQUENCE, timeout)); 122 for (Enumeration<?> e = command.getRequirements(); e.hasMoreElements(); ) { 123 requires((Subsystem) e.nextElement()); 124 } 125 } 126 127 /** 128 * Adds a new child {@link Command} to the group. The {@link Command} will be started after all 129 * the previously added {@link Command Commands}. 130 * 131 * <p>Instead of waiting for the child to finish, a {@link CommandGroup} will have it run at the 132 * same time as the subsequent {@link Command Commands}. The child will run until either it 133 * finishes, a new child with conflicting requirements is started, or the main sequence runs a 134 * {@link Command} with conflicting requirements. In the latter two cases, the child will be 135 * canceled even if it says it can't be interrupted. 136 * 137 * <p>Note that any requirements the given {@link Command Command} has will be added to the group. 138 * For this reason, a {@link Command Command's} requirements can not be changed after being added 139 * to a group. 140 * 141 * <p>It is recommended that this method be called in the constructor. 142 * 143 * @param command The command to be added 144 * @throws IllegalUseOfCommandException if the group has been started before or been given to 145 * another command group 146 * @throws IllegalArgumentException if command is null 147 */ 148 public final synchronized void addParallel(Command command) { 149 requireNonNull(command, "Provided command was null"); 150 validate("Can not add new command to command group"); 151 152 command.setParent(this); 153 154 m_commands.addElement(new Entry(command, Entry.BRANCH_CHILD)); 155 for (Enumeration<?> e = command.getRequirements(); e.hasMoreElements(); ) { 156 requires((Subsystem) e.nextElement()); 157 } 158 } 159 160 /** 161 * Adds a new child {@link Command} to the group with the given timeout. The {@link Command} will 162 * be started after all the previously added {@link Command Commands}. 163 * 164 * <p>Once the {@link Command Command} is started, it will run until it finishes, is interrupted, 165 * or the time expires, whichever is sooner. Note that the given {@link Command Command} will have 166 * no knowledge that it is on a timer. 167 * 168 * <p>Instead of waiting for the child to finish, a {@link CommandGroup} will have it run at the 169 * same time as the subsequent {@link Command Commands}. The child will run until either it 170 * finishes, the timeout expires, a new child with conflicting requirements is started, or the 171 * main sequence runs a {@link Command} with conflicting requirements. In the latter two cases, 172 * the child will be canceled even if it says it can't be interrupted. 173 * 174 * <p>Note that any requirements the given {@link Command Command} has will be added to the group. 175 * For this reason, a {@link Command Command's} requirements can not be changed after being added 176 * to a group. 177 * 178 * <p>It is recommended that this method be called in the constructor. 179 * 180 * @param command The command to be added 181 * @param timeout The timeout (in seconds) 182 * @throws IllegalUseOfCommandException if the group has been started before or been given to 183 * another command group 184 * @throws IllegalArgumentException if command is null 185 */ 186 public final synchronized void addParallel(Command command, double timeout) { 187 requireNonNull(command, "Provided command was null"); 188 if (timeout < 0) { 189 throw new IllegalArgumentException("Can not be given a negative timeout"); 190 } 191 validate("Can not add new command to command group"); 192 193 command.setParent(this); 194 195 m_commands.addElement(new Entry(command, Entry.BRANCH_CHILD, timeout)); 196 for (Enumeration<?> e = command.getRequirements(); e.hasMoreElements(); ) { 197 requires((Subsystem) e.nextElement()); 198 } 199 } 200 201 @Override 202 @SuppressWarnings("MethodName") 203 void _initialize() { 204 m_currentCommandIndex = -1; 205 } 206 207 @Override 208 @SuppressWarnings({"MethodName", "PMD.AvoidReassigningLoopVariables"}) 209 void _execute() { 210 Entry entry = null; 211 Command cmd = null; 212 boolean firstRun = false; 213 if (m_currentCommandIndex == -1) { 214 firstRun = true; 215 m_currentCommandIndex = 0; 216 } 217 218 while (m_currentCommandIndex < m_commands.size()) { 219 if (cmd != null) { 220 if (entry.isTimedOut()) { 221 cmd._cancel(); 222 } 223 if (cmd.run()) { 224 break; 225 } else { 226 cmd.removed(); 227 m_currentCommandIndex++; 228 firstRun = true; 229 cmd = null; 230 continue; 231 } 232 } 233 234 entry = m_commands.elementAt(m_currentCommandIndex); 235 cmd = null; 236 237 switch (entry.m_state) { 238 case Entry.IN_SEQUENCE: 239 cmd = entry.m_command; 240 if (firstRun) { 241 cmd.startRunning(); 242 cancelConflicts(cmd); 243 } 244 firstRun = false; 245 break; 246 case Entry.BRANCH_PEER: 247 m_currentCommandIndex++; 248 entry.m_command.start(); 249 break; 250 case Entry.BRANCH_CHILD: 251 m_currentCommandIndex++; 252 cancelConflicts(entry.m_command); 253 entry.m_command.startRunning(); 254 m_children.addElement(entry); 255 break; 256 default: 257 break; 258 } 259 } 260 261 // Run Children 262 for (int i = 0; i < m_children.size(); i++) { 263 entry = m_children.elementAt(i); 264 Command child = entry.m_command; 265 if (entry.isTimedOut()) { 266 child._cancel(); 267 } 268 if (!child.run()) { 269 child.removed(); 270 m_children.removeElementAt(i--); 271 } 272 } 273 } 274 275 @Override 276 @SuppressWarnings("MethodName") 277 void _end() { 278 // Theoretically, we don't have to check this, but we do if teams override 279 // the isFinished method 280 if (m_currentCommandIndex != -1 && m_currentCommandIndex < m_commands.size()) { 281 Command cmd = m_commands.elementAt(m_currentCommandIndex).m_command; 282 cmd._cancel(); 283 cmd.removed(); 284 } 285 286 Enumeration<?> children = m_children.elements(); 287 while (children.hasMoreElements()) { 288 Command cmd = ((Entry) children.nextElement()).m_command; 289 cmd._cancel(); 290 cmd.removed(); 291 } 292 m_children.removeAllElements(); 293 } 294 295 @Override 296 @SuppressWarnings("MethodName") 297 void _interrupted() { 298 _end(); 299 } 300 301 /** 302 * Returns true if all the {@link Command Commands} in this group have been started and have 303 * finished. 304 * 305 * <p>Teams may override this method, although they should probably reference super.isFinished() 306 * if they do. 307 * 308 * @return whether this {@link CommandGroup} is finished 309 */ 310 @Override 311 protected boolean isFinished() { 312 return m_currentCommandIndex >= m_commands.size() && m_children.isEmpty(); 313 } 314 315 // Can be overwritten by teams 316 @Override 317 protected void initialize() {} 318 319 // Can be overwritten by teams 320 @Override 321 protected void execute() {} 322 323 // Can be overwritten by teams 324 @Override 325 protected void end() {} 326 327 // Can be overwritten by teams 328 @Override 329 protected void interrupted() {} 330 331 /** 332 * Returns whether or not this group is interruptible. A command group will be uninterruptible if 333 * {@link CommandGroup#setInterruptible(boolean) setInterruptible(false)} was called or if it is 334 * currently running an uninterruptible command or child. 335 * 336 * @return whether or not this {@link CommandGroup} is interruptible. 337 */ 338 @Override 339 public synchronized boolean isInterruptible() { 340 if (!super.isInterruptible()) { 341 return false; 342 } 343 344 if (m_currentCommandIndex != -1 && m_currentCommandIndex < m_commands.size()) { 345 Command cmd = m_commands.elementAt(m_currentCommandIndex).m_command; 346 if (!cmd.isInterruptible()) { 347 return false; 348 } 349 } 350 351 for (int i = 0; i < m_children.size(); i++) { 352 if (!m_children.elementAt(i).m_command.isInterruptible()) { 353 return false; 354 } 355 } 356 357 return true; 358 } 359 360 @SuppressWarnings("PMD.AvoidReassigningLoopVariables") 361 private void cancelConflicts(Command command) { 362 for (int i = 0; i < m_children.size(); i++) { 363 Command child = m_children.elementAt(i).m_command; 364 365 Enumeration<?> requirements = command.getRequirements(); 366 367 while (requirements.hasMoreElements()) { 368 Object requirement = requirements.nextElement(); 369 if (child.doesRequire((Subsystem) requirement)) { 370 child._cancel(); 371 child.removed(); 372 m_children.removeElementAt(i--); 373 break; 374 } 375 } 376 } 377 } 378 379 private static class Entry { 380 private static final int IN_SEQUENCE = 0; 381 private static final int BRANCH_PEER = 1; 382 private static final int BRANCH_CHILD = 2; 383 private final Command m_command; 384 private final int m_state; 385 private final double m_timeout; 386 387 Entry(Command command, int state) { 388 m_command = command; 389 m_state = state; 390 m_timeout = -1; 391 } 392 393 Entry(Command command, int state, double timeout) { 394 m_command = command; 395 m_state = state; 396 m_timeout = timeout; 397 } 398 399 boolean isTimedOut() { 400 if (m_timeout == -1) { 401 return false; 402 } else { 403 double time = m_command.timeSinceInitialized(); 404 return time != 0 && time >= m_timeout; 405 } 406 } 407 } 408}