-
Notifications
You must be signed in to change notification settings - Fork 0
Defining Commands
Commands are at the core of a VexOS program design. They are the main mechanism for defining the behavior of a robot, as opposed to Subsystems which are mainly about structure.
While Subsystems are singletons, meaning that the class and the single instance can almost be considered to be the same thing, Commands are implemented as full-fledged classes where many distinct instances can be created. Thus, when you define a type of Command you are actually defining its class, and VexOS takes are of creating the individual instances.
For example, the code below creates two instances of a Command class named SetIntake
and feeds each a different argument. When each of these Commands is run they order the intake to the desired direction.
Command* stopCmd = Command_new(&SetIntake, IntakeDirection_Stop);
Command* suckCmd = Command_new(&SetIntake, IntakeDirection_Suck);
The stopCmd
and suckCmd
variables (which are pointers to the Command instance data created by Command_new(...)
) are distinct, but they are both defined by the same Command class 'SetIntake'. As explained in the Defining the Robot section, all VexOS objects must be referenced through pointers and all Command classes and Subsystems must be turned into pointers (via the & operator) when used in VexOS.
VexOS also has the concept of a CommandGroup, which is a type of Command that aggregates together a number of other commands and runs them either in sequence or in parallel. For more information, see Using CommandGroups.
The page covers how to define new Command classes, by first declaring them in Robot.h
, then defining them in the a dedicated C source code file. The main difference from Defining Subsystems is that arguments and instance data fields (data specific to an instance of a class) must be taken into account. Once the syntactical mechanics of defining a Command class are established, implementing the actual behavior of the Command will be explained.
For each Command class you create for your robot, entries must be made in Robot.h
that:
- Declare the existence of the CommandClass variable.
- Declare any data types (such as enums) used as arguments for the CommandClass.
- Optionally, declare any public methods used to manipulate instances of a CommandClass, though this is not a common requirement.
An example of a declaration of the SetIntake
Command class in Robot.h
is:
DeclareCommandClass(SetIntake);
typedef enum {
IntakeDirection_Stop,
IntakeDirection_Suck,
IntakeDirection_Blow
} IntakeDirection;
The DeclareCommandClass(SetIntake)
pre-processor macro declares the SetIntake
variable, which is an extern
reference to a variable of type CommandClass
. Next, we declare the enumeration that is used as the argument to Command_new(&SetIntake, _dir_)
. This enumeration may have already been declared with the Intake
Subsystem, since it is common to share those types of values between Commands and Subsystems.
A minimal Command class declaration is a simple as:
DeclareCommandClass(DriveWithJoystick);
This command class does not have any custom argument types.
VexOS comes with a number of Command classes already built-in to the core library. These are declared in VexOS.h
and may be used in your robot without declaring them in Robot.h
. These Command classes are mainly concerned with waits and timing, since those functions do not explicitly depend on any Subsystem of your robot. For more, see Built-in Commands.
Once the Command class has been declared in Robot.h
, you must create the actual implementation. By convention, each Command class should be defined in its own file. If using easyC, the file should be titled cmd__NAME_.c
, where NAME is the name of the CommandClass, in [CamelCase][]. If not using easyC, then the suggested practice is to make a commands/
directory in your robot project and place NAME.c files (without a prefix) in that directory.
The definition of the (very simple) SetIntake
Command class is given below:
//
// cmd_SetIntake.c
//
#include "CommandClass.h"
#include "Robot.h"
/********************************************************************
* Class Definition *
********************************************************************/
DefineCommandClass(SetIntake, {
IntakeDirection dir;
});
static void constructor(va_list argp) {
self->fields->dir = (IntakeDirection) va_arg(argp, int);
require(&Intake);
}
static void initialize() {
Intake_setDirection(self->fields->dir);
}
static void execute() { }
static bool isFinished() {
return true;
}
static void end() { }
static void interrupted() { }
A Command class will always include CommandClass.h
, as this header contains all of the magic needed to create the CommandClass variable. CommandClass.h
should only be include in Command class definition C files and should not be included in any other type of file. This header is part of VexOS and should have been imported into your project previously, see Using VexOS in easyC. Not that you should generally never include Hardware.h
in your Command class definitions. A good VexOS program design will place all hardware access in either Subsystems or the main Robot object. As a general rule, Commands should never touch hardware.
The next include is Robot.h
, which contains the declaration of this Command class, as well as the Subsystems and their public methods, which will be called from Commands.
The next component is the DefineCommandClass(name, { fields })
pre-processor macro. This is what actually creates the variable of type CommandClass and links it up to everything else defined in the C file. The first argument is the name of the Command class. This should exactly match the name given to DeclareCommandClass(name)
in the header file.
The next argument is a list of instance field declarations. Because there may be multiple instances of a Command class created (such as SetIntake
instances for Stop, Suck and/or Blow), internal values cannot be stored in file-level variables as they are for Subsystems and the Robot singletons. Instead, these values must be stored elsewhere, in a location that exists separately for each Command instance. These field values are declared inside of the curly-braces { } passed to the second argument of DefineCommandClass(...)
. Field declarations look exactly like variable declarations, though they cannot have initializes (i.e. int i = 0;
is not allowed, only int i;
). If multiple fields are needed, they must be separate by semi-colons. As you will see shortly, these values become visible as a fields
pointer for a Command instance.
What is actually going on here, behind the scenes, is that the pre-processor macro is creating a C structure, named struct Fields
that contains all of the values declared in the curly braces. VexOS allocates memory for this structure for each instance of your command class that is created.
The command constructor is a function that is called when a Command instance is created by a call to Command_new(&class, ...)
. Unlike the constructors for Subsystems and the Robot, which are called as part of system start-up, the Command constructor is not called until the Command class is instantiated.
Another difference is that there is an argument passed to the constructor of type va_list
. The va_list
the (rather primitive) way the C language uses to pass multiple undefined arguments to a function. More information is available in the description of stdarg.h, the standard header file that defines the behavior. This va_list
contains all of the arguments passed to Command_new(&class, ...)
after the class name. It is the job of your constructor to read these arguments out of the va_list
and do something with them, typically they are placed into fields for later use.
In the example below, the first argument is read from the va_list
and stored into the IntakeDirection dir;
instance field. As you can tell from the example, the field can be accessed with the expression self->fields->dir
. The variable self
, which is automatically present during all standard Command method calls, is a pointer to the current command instance. This is required, because remember, the Command class being defined can be used to create multiple instance commands. All of these copies shared the same behavior code (the functions defined here) but different data (the instance pointer and it's fields). When VexOS calls the command methods, it makes sure the self
pointer is set to the right place. If this is too complex, simply remember that you can read and write to the fields you declare using the expression self->fields->NAME
, where NAME is the name of the field.
If a Command class does not have any fields (which is fairly common), the DefineCommandClass()
call must be made with empty curly braces, as follows:
DefineCommandClass(DoVeryLittle, { });
This is due to limitations of the C pre-processor.
If you've studied the stdarg.h documentation, you may wonder why we didn't use the statement va_arg(argp, IntakeDirection)
to read the dir
value out. Instead, it was read out as an int
and cast to an IntakeDirection
enumeration. The reason for this is that GCC, the open source compiler used by easyC does not put anything in a va_list
smaller than the data type int
. Most enums are indeed smaller than a standard integer, so they get promoted to type integer. If you tried that statement, you'd get the following warning:
Worse yet, if you ignore this warning, you program would crash when you try to run it, so it is actually way less cool than a normal warning. Other C compilers do not do this, so the statement passing the enum type to va_arg()
would work just file. However, with Vex we don't have a choice of using other compilers. Actually, the cast to IntakeDirection
is not required, we could easily have written self->fields->dir = va_arg(argp, int);
, but I feel that is not as clear to the reader.
The next element are the Subsystem requirement statements. This call takes a pointer to a Subsystem that this Command requires control of to run. Since the method takes a pointer, the & operator must be used with the Subsystem variables. One require(&sys);
line should be present for each Subsystem that is needed.
This section describes the standard methods that must be implemented for each Command class.
- initialize - called when the Command starts running, not that Commands can be restarted after they stop, so it is possible for this method to be called multiple times. In our example, the method passes on the change of state to the Intake subsystem using its public methods.
- execute - while the command is running, this method is called once per scheduler loop. In our example, this does nothing because the initialize function does all the work.
- isFinished - returns whether the command is finished and should terminate. Since the SetIntake command does everything in initialize, it is always ready to terminate after it has been started and run once.
- end - is called once isFinished returns true and the command is stopping. This is typically where any clean-up is done (such as putting the robot into a "safe" state, since you don't know what command will run next).
- interrupted - this method is called if another command tries to take over one of the Subsystems this command requires. If that happens, this command gets interrupted.
The self
automatic variable is only available in functions listed above and the Command constructor. If you were to define a public method, self
will be undefined (either NULL or a random value). For information on how to deal with this, see Declaring Command and Button Public Methods.