Drivers: look at the Driving section at the end.
Everywhere else in this document is intended for programmers.
The latest version of the Jeffrey robot code changes the function of the code at a fundamental level. It takes what wpilib already provides with command-based programming, and builds on top of it adding a swarm of new features. With that said, it is highly recommended that any new programmers read the following sections of the wpilib documentation:
And most importantly...
Starting here and reading more about sensors and other things will also serve you well, but the 3 items above are the most important.
- Setting up a workspace
- Programming!
- Scheduler
- Subsystems
- Commands
- Drive train
WebSocketTableServer
Robot
(main class)Notifier
- Checklists for adding new features
- Auto
- Removed classes
- Driving
As of now, we don't use Eclipse anymore. We use a combination of Intellij IDEA for the development environment and GradleRIO for the workspace.
Make sure you have IntelliJ and Java 8 installed. GradleRIO is already built-in to this repository. Clone this repository and open a terminal in the root folder. Then just run ./gradlew idea
(gradlew idea
if you are on windows) and wait for GradleRIO to do the magic. You should also talk to the programming captain about importing settings as well.
That's the magic command. The nice thing about GradleRIO is you don't even have to load up your IDE to push code. However, in case you do have IntelliJ open, it is possible to set up the "Run code" button to do this. We won't get into that here yet, but you can talk to your friendly neighborhood programming captain for more information.
This project builds on top of the command-based programming already present in wpilib. This documentation assumes you have already read that section and are reasonably familiar with it. This simply discusses what is different between our code and what is documented there.
The Midcoast Maineiacs Scheduler is similar to the wpilib Scheduler, with some added benefits:
- The Scheduler can be enabled or disabled by the code; it is safe to assume that
Scheduler.enabled = false
will be equivalent to the robot being fully disabled. At the moment,Scheduler.enabled
simply reflects the mobility of the robot as indicated by the RSL light, but other special conditions can be added as new features as need be. - When the Scheduler is "disabled," commands are simply frozen if
willRunWhenDisabled
is false. They will not be cancelled by the Scheduler, howeverRobot
may cancel them in some cases. - The Scheduler keeps track of which command it is running, so it is easy to see what command has led to a specific code segment being called via the use of
Scheduler.getCurrentCommand()
. - It also runs using a
Timer
on a separate thread with exact 50Hz timing, meaning it is safe to use the iteration of commands for timing. WPILib traditionally can vary the timing based on the behavior of the network connection, but the Timer is independent of the network. - Because we have full control of the Scheduler, we can modify it easily to implement other features. This probably won't be the last time you hear mention of the Scheduler.
Also, the isFinished()
value is checked twice for every iteration. Once before executing (unless the command hasn't started yet, it is guaranteed that initialize
and execute
will be called exactly once before isFinished
is checked), and once after executing. See order below:
- If this isn't the first run, check
isFinished()
; stop command if true - Check
isCancelled()
; stop command if true - If this is the first run, run
initialize()
- Run
execute()
- Check
isFinished()
; stop command if true - Check
isCancelled()
; stop command if true - 20ms (minus the time it took to process the command) delay
The Scheduler class extends TimerTask
and can be instantiated and used for creating a Timer
. This is already done by Robot
. All methods and variables (except for run()
, which makes it a valid TimerTask
) are static.
Commands are started by Scheduler.add(Command)
. Command.start()
should work for all commands still, but some commands may need to have their start()
methods manually changed to use add()
(see starting commands below).
All subsystems extend an MMSubsystem
class, which extends the WPILib Subsystem
class. This expands upon the normal WPILib subsystem, adding new features:
- All subsystems can be "controlled" by a command. The most important thing to note here is when you write your subsystems, make sure the methods that actually actuate the motors check that
verifyResponse()
returnstrue
before doing anything. TheverifyResponse()
makes sure that if the method was called by a command, that command actually has control of the subsystem, and if it wasn't called by a command, the subsystem isn't currently being controlled by a command. - All subsystems have a
stop()
method. This is abstract inMMSubsystem
, which means when you make a new subsystem, IntelliJ will scream at you until you overridestop()
. This method should unconditionally stop all actuators, without checkingverifyResponse
. MMSubsystem
also provides acontrolledByTeleop()
method which returns true when teleop is enabled (according to the Scheduler) and the subsystem is currently not controlled by any command. This implies that the subsystem should be responding to controller inputs from the driver station.
If this doesn't make sense, just keep reading.
Commands are probably the most complicated part of this project (except maybe DriveTrain, but that will vary year-to-year). There are two types of commands, active commands and passive commands. Active commands all extend the new MMCommand
class (which extends the WPILib Command
class), while passive commands all extend the WPILib Command
class and not MMCommand
.
The primary difference between active and passive commands is that passive commands are not allowed to...
- move (or cause to move) any subsystem motors (enforced by
verifyResponse()
) - take control of a subsystem (the
takeControl()
method only acceptsMMCommand
as an argument) - start any active command (they can still start other passive commands, enforced by
Scheduler.add()
)
Trying to do any of these will either result in your code crashing, or the code not compiling in the first place.
At the time of writing, Notifier
is the only passive command. All other commands (including ones like Series
) are active.
Commands are started by Scheduler.add()
, as mentioned above. MMCommand
overrides start()
, causing it to automatically use Scheduler.add()
. Because active commands extend MMCommand
, no more work needs to be done there. Passive commands, however, will by default use WPILib's start()
method which will simply do nothing with our Scheduler. When programming passive commands, make sure you override start()
so that it just uses the correct Scheduler. See Notifier
for an example.
When that's said and done, commands can be started like normal with command.start()
.
Active commands extend MMCommand
and can do all of the things listed above that passive commands cannot do. These are the "do something" commands.
Note: There is no Teleop command. Teleop controls are handled by
Robot.teleopPeriodic()
.
The MMCommand
class adds a host of new features:
- When one command starts another, commands will keep track of what command started them (their parents) and what commands they start (their children).
- Commands can take control of subsystems. When a command has control, this guarantees that nothing but that command can take control of the subsystem. When a command has control of a subsystem, that implies that all of it's children also have control. It is recommended that if a command will start a command that will move a subsystem, that the parent command claims control. A child command will not take control away from its parent. It is recommended to take control during the
initialize
phase with something likedriveTrain.takeControl(this)
. MMCommand
provides ashouldCancel()
method which checks if the Robot kill switch (theA
button at the time of writing) or a command's parent has stopped running.- There is a default
isFinished()
implementation that checks if the command has timed out or ifshouldCancel()
returns true. It is recommended that if you are writing a command that controls a subsystem, you have isFinished return true ifsuper.isFinished
(orshouldCancel
, depending on your needs) returns true or if you have lost control of any of the subsystems you need. E.g.return !Robot.driveTrain.controlledBy(this) || super.isFinished()
. - There are two timing functions available.
setTimeout()
is just WPILib's default implemantation.timeout()
is a custom implementation that usessetTimeout()
, but the timer will pause when the command is frozen (e.g. when the Scheduler is disabled). - Sometimes, you will want to start a command as your last child command, and then stop running after that command ends. This can be done by starting the command, then calling
releaseForChildren()
. WhenreleaseForChildren()
is called, a flag is set, which causesshouldCancel()
to return true if there are no still-running child commands. - There is a new
resume()
method. This can be overridden, and is called whenever a command is frozen, then unfrozen. It should be assumed that all subsystems have had theirstop()
method called since the last time the command has had a chance to move them. Any subsystem instructions that were given duringinitialize()
that would be cancelled bystop()
(and not re-run byexecute()
) should be repeated by overriding this method. - There is a default
end()
implementation that will find any subsystems that this controls, and relinquish control, stopping the subsystem. It is recommended that if your command at any point takes control of another subsystem, that you either leave this method as-is or callsuper.end()
when overriding it.
Note: Be careful when starting commands within commands. If you start a command within an
execute
block, make sure you don't accidentally create a condition where the parent command will flood the scheduler with tons of child commands. Make use ofreleaseForChildren
and therequireChildren
method and variable.requireChildren
will start asfalse
and be set totrue
afterreleaseForChildren
is called. See the code forGear
for an example of this.
When the robot turns on, it will automatically detect whether or not it's at a competition based on if it's connected to the FMS (or the DS is in practice mode). If so, it will enable a "competition mode" flag which can change the behavior of the robot.
The biggest difference is that when the Scheduler gets disabled, passive commands behave the same way (meaning they keep running), while active commands will not get cancelled, as they would otherwise. Instead they get "frozen." This keeps them alive but they will not get executed on by the Scheduler. They will remain in the schedule, dormant until the Scheduler is re-enabled.
As well, if the timeout
method is used for timing, the timing will pause while the command is dormant. Note that this does not happen if the WPILib setTimeout
method is used. When that is used, the command will continue to timeout when the robot is disabled, which may be desired in some cases.
When the Scheduler is re-enabled, the resume
method of the command is called, then execution will resume as normal. This means that in competition mode, disabling the robot will not cancel the commands that are running. You must hit the kill switch (A button at the time of writing) to force cancel all commands. Otherwise, the robot may start moving again as soon as it gets re-enabled, even just in Teleop.
For safety, a warning is sent to the DS whenever the Scheduler is disabled while in competition mode, which would mean any commands that were running were not cancelled.
The Scheduler will continue to check
isCancelled()
even when a command is dormant, and if the command is cancelled, it will be run in order to allow the command to shut down. This can happen even when the robot is disabled.
Competition mode can be forcibly enabled by changing the static FORCE_COMPETITION
constant in Robot.java
to true
.
Important: this is only to be used for testing purposes, and a scary warning will be sent to the DS if this is enabled. Do not do this unless you have to test a competition-only feature.
A Series
is an active command that simply runs each argument it given in it's constructor in parallel. It is an active command, as it has the ability to start other active commands. It replaces the functionality of CommandGroup
.
It will not take control of a subsystem, but can inherit control from its parents and will pass that control down to its children. This is the normal behavior of active commands.
It will stop running once all of its children have finished running, and will not start a command before the previous one has finished. All commands will always be executed in order of how they are specified in the arguments.
There is a Series.Parallel
class which behaves similarly to the Series
, but it will run all commands at the same time, and stop itself once all commands have finished. A Series
can be fed into a Parallel
and vice versa. The Parallel
class extends Series
, which extends MMCommand
.
Series
andParallel
can handle passive commands, active commands, or a combination of the two.
The DriveCommand
is a convenient way of moving the drive train in a particular manner. Currently there are two forms of the constructor. One form accepts two doubles and will drive straight (using the gyro to maintain heading) at a set speed for a set period of time. The other form accepts three doubles and will set each side of the drive train to an individually specified speed for a set period of time. The timing uses the custom timeout()
method, meaning it will be paused when the command is frozen.
Example line of code that drives forward at 50% power for 2 seconds, stops for 5 seconds, and drives back at 50% power for 2 seconds:
(new Series(new DriveCommand(0.5, 2), new DriveCommand(0, 5), new DriveCommand(-0.5, 2))).start();
The DriveTrain
is a subsystem that extends MMSubsystem
, and adds some other features.
The drive train provides a setAutopilot
function which must be called by an active command. This function will automatically hand over control to whatever command called it, and put the drive train into autopilot mode. In this mode, the Scheduler calls the drive train's updateAutopilot
function which will follow the instructions given by setAutopilot
. The autopilot mode will be cancelled when either A) the command dies or B) setAutopilot
is called with different parameters. As of the time of writing, the autopilot will simply drive in a straight line, using the gyro to maintain a straight heading. More features may be added in the future.
What sets this apart from normal control is the command doesn't actually do anything to move the motors. In fact, there doesn't even need to be an execute
block for a command to take advantage of autopilot. All the command has to do is stay alive for however long it wants the autopilot to run, and stop running when it wants autopilot to stop. A simple use of this is to simply call setAutopilot()
and then timeout()
in the initialize()
method, and let the autopilot do it's thing and let the default implementation of isFinished()
provided by MMCommand
to automatically stop when the timeout ends. The Scheduler will take care of constantly updating the autopilot feature to allow the drive train to do it's job.
Example command that drives forward at 50% power for 1.5 seconds:
class Drive extends MMCommand {
@Override
protected void initialize() {
Robot.driveTrain.setAutopilot(0.5); // 50% power
timeout(1.5); // 1.5 seconds
}
@Override
protected void isFinished() {
return super.isFinished() || !Robot.driveTrain.controlledBy(this);
}
}
One form of DriveCommand
uses autopilot to stay straight.
Autopilot will be frozen when the scheduler is disabled and will restart unhindered when the scheduler resumes, assuming the command is still running. It is not necessary to add a
resume()
method to handle autopilot.
The
setAutopilot
function will automatically grant control of the drive train to the command. AddingdriveTrain.takeControl(this)
is redundant.
The drive train implements a state system with very complicated-looking code. It simply implements the following four states (from lowest to highest priority).
DISABLED
means the drive train is currently not doing anythingTELEOP
means the drive train is currently being controlled by the teleop controls inteleopPeriodic()
COMMAND
means the drive train is being controlled by a command (that is not using autopilot)AUTOPILOT
means the drive train is being controlled by its autopilot functionality
DriveTrain
overrides takeControl
, relinquishControl
and other methods in order to maintain this state system. The drive train will call Robot.notifyDriver()
(see below) any time the state changes and the change is not simply between COMMAND
and AUTOPILOT
.
NOTE: This does not reflect the actual state of the robot. This is what the drive train thinks it's doing.
E.g. if the auto command ends before the end of the autonomous period, the drive train will switch to the
DISABLED
state, even though the robot is still enabled.If there is an autonomous command running and the robot is disabled (and the command is frozen, not cancelled), the drive train will stay in the
COMMAND
orAUTOPILOT
state. This doesn't mean it's moving (or trying to move), it just means it's still under the control of a command.
The drive train also has an acceleration curve that takes effect whenever driveLeftCurved
or driveRightCurved
is used. It mitigates sudden acceleration by limiting how much the motor speed can change each time the method is called.
The WebSocketTableServer
behaves similarly to the wpilib NetworkTables server. Initialize it with a name (for logging purposes), and a port. This will host a local server listening on port
, which clients can connect to. What is a WebSocketTable and how do I use it?
The DashboardServer
runs on port 5800
. This is connected to by the MMDashboard automatically. It contains data to be displayed to the drivers, debugging data, and the name of the robot, Jeffrey
, to allow the MMDashboard to recognize how to interpret the data. It is not recessive.
Updates to this table are generally done in robotPeriodic
. It is stored as a static dashboard
variable in the main Robot class. To send a value, simply use the set...
methods provided by WebSocketTableServer
. Example:
@Override
protected void robotPeriodic() {
// ...
dashboard.setBoolean("enabled", Scheduler.enabled);
// ...
}
The MMDashboard won't show new values by default. The webpage must be edited to recognize these values, however, clicking on the compass will show a debug screen which will show all of the values on the table in their raw form, even if they aren't coded to be used by the page. This is usable for debugging code. Anywhere in the code, you can reference Robot.dashboard
to send a value to the DS for debugging.
The VisionServer
runs on port 5506
. This is connected to by the Raspberry Pi, which handles vision processing. This server is recessive, meaning it will update the table to the contents of whichever client has most recently connected whenever a new connection is opened.
The Robot
class is the main class that ties everything together. It also contains the functionality formerly present in the Teleop
and OI
classes. It...
- instantiates all subsystems
- calibrates sensors
- manages the MMDashboard
- instantiates and starts the Scheduler
- ensures that the Scheduler is put in the right state (
enabled
andteleop
statuses) whenever the robot state changes - resets sensors at the beginning of the match
- detects whether or not a match is in progress (or if the robot is just being enabled or disabled manually)
- makes sure that commands are cancelled if they need to be when the robot is disabled
- NOTE: This does not happen in competition mode. This means that at competition, if the autonomous routine is still running at the end of auto, it will keep running throughout teleop.
- handles all teleop controls
- handles when to take control away from the autonomous commands and hand it over to the driver
- starts and stops high-level autonomous commands
The Notifier
is a (at the time of writing, the only) passive command in the project. Any time it is started, if there isn't already a Notifier
running, it will send two rumble pulses to the controller to notify the driver. If there already is one running, it will cancel the duplicate Notifier
and simply add one more pulse to the old one.
There is a static Robot.notifyDriver()
method which simply creates and starts a Notifier
. This method will return immediately, even before the notifier finishes, and as such, will not interrupt the flow of the code calling it.
The
Notifier
is a passive command that is set to run when disabled, and will be fully functional no matter what state the robot is in.
- Add public instance variables for your actuators and sensors (instantiate them directly in the subsystem class in the same way
DriveTrain
does) - Add public methods for controlling your subsystem (make sure you check
verifyResponse()
) - Add a public
stop()
method which stops all actuators (don't checkverifyResponse()
) - Add static variable to
Robot
containing subsystem - At the top of
Robot.robotInit()
, add the subsystem to thesubsystems
array - Test!
- Decide whether a command is active or passive
- If it is active and it needs to control a subsystem, make sure
initialize()
takes control of the subsystem andisFinished()
returns true if it loses control of the subsystem - If it uses the drive train autopilot, make sure you do that in
initialize()
- If it is active, make sure
isFinished()
returns true if at leastshouldCancel()
(if notsuper.isFinished()
) is true - If it is passive, make sure to override
start()
to containScheduler.add(this)
- Test!
- Find out where to put the new controls in
teleopPeriodic()
. If there are already controls for that subsystem, then find theif (subsystem.controlledByTeleop()) {
block for that subsystem. Otherwise, make one (make sure it's after theif (!Scheduler.enabled) return true;
line). - Code the controls. Check the mappings to make sure it won't conflict with other controls and make any modifications necessary. The
joystick
variable is what you need for the joystick, and the subsystems are also stored asRobot
variables. Anywhere where you callgetRawAxis(number)
orgetRawButton(number)
, put in a comment that specifies what button or axis that number is referring to. - Update the mappings to reflect your changes (in the giant comment in
Robot.java
and in the Driving > Controls section of this document). - Test!
The auto routine at the time of writing for the 2017 STEAMWORKS challenge has three modes. The auto routine is handled by the Auto
command and the mode is specified by an enumerator containing the possible modes.
PLAY_DEAD
does nothing. It's just ignored.GEAR
runs theGear
command. It will drive at 50% power for half of a second before running the command if the robot starts on one of the side positions.SURGE
drives at 60% power for 2 seconds if the robot starts on one of the side positions. It will do nothing in the middle.
These auto routines drive the robot backwards, meaning they assume that the robot placed with the gear holder facing outward, away from the wall. This should always be the starting configuration of the robot.
Auto
and Gear
demonstrate good use of Series
, DriveCommand
, and releaseForChildren()
. Look at them for examples of usage of these features.
If you have looked at some older code or code for previous years, you may have noticed a few extra classes that have been merged elsewhere. Here's the three big ones:
Teleop
is no longer a command. All of the code that used to be in theTeleop
command(s) is now inRobot.teleopPeriodic()
RobotMap
is no longer used. It had no purpose. The motor controllers and sensors are now instantiated by the subsystems themselves.OI
is also no longer used. The joysticks are now completely handled within theRobot
class.Robot.joystick
is a lot nicer thanRobot.oi.getDriverJoystick()
, especially since now that the teleop controls are handled inteleopPeriodic
, simplyjoystick
is usually sufficient.
Jeffrey v3.0
uses one controller for driving and the MMDashboard for feedback.
Button | Description |
---|---|
Sticks |
Drive using tank controls. |
Left bumper |
Toogle motor speed between 50% and 100%. Defaults to 100%. |
A |
Kill all autonomous commands. |
Y |
Attempt to autonomously put a gear on the peg. If it cannot see the peg, it will scan in a direction chosen based on the location of the driver console |
D-Pad |
Drive arcade-style. Tank drive will not take effect while D-Pad buttons are held down. |
Right stick X-axis |
More fine-tuned turning control for POV arcade drive. |
Right trigger |
Throttle for D-Pad arcade drive, full speed. Robot will not move if D-Pad is used and this (or the left) trigger is not pulled. |
Left trigger |
Throttle for D-Pad arcade drive, half speed. Robot will not move if D-Pad is used and this (or the right) trigger is not pulled. |
Right bumper |
Climb up at full power. |
B |
Climb up at half power. Use to hold position at the top of the rope at competition. |
X |
Climb down at half power. Use at demonstrations. |
Right trigger |
Climb up. The distance at which you pull the trigger controls how fast the climber works. Will not work if D-Pad is held down, as this becomes the throttle. |
Left trigger |
Climb down. Use at demonstrations. Same rules as above. |
Back |
Disable the robot. Motors will stop, but the RSL light will stay blinking. |
Start |
Enable the robot (if the RSL light is blinking). |
.................. |
TODO