-
Notifications
You must be signed in to change notification settings - Fork 12
Coding with Toast
Toast runs on top of the WPILib library for FRC, which is very well understood and universal among most FRC Teams. WPILib provides tutorials for things like Motors, Digital IO, and Control Systems on their Screensteps Page, so in the sake of not repeating instructions, code in the WPILib domain will not be covered in the Toast tutorials unless Toast has patched its front-end behavior.
Your module is the code that your write for your Robot, or to be adopted by others. There is a wiki entry on creating your Java Module here.
There are multiple ways to implement a robot module, with BaseClasses available for you to extend. These BaseClasses are: ToastModule, ToastStateModule, and IterativeModule.
HotPlate uses IterativeModule, and is often chosen as the boilerplate for module development. IterativeModule will give you an interface very similar to WPILib's IterativeRobot. IterativeModule provides methods for you to override relating to Robot states. Use this for your main, robot module.
ToastStateModule is similar to IterativeModule, but provides ticking and transitioning methods that are not filtered, that is, take RobotState as an argument and give it to you directly. It is mostly a wrapper for the StateListener interface.
ToastModule is the most barebones of the modules. ToastModule doesn't provide any state activity, and is mostly used for 'passive' modules that don't tick. Use this if you're making a library module.
Any module class has two common methods that you MUST overload, getModuleName() and getModuleVersion().
getModuleName()
will return the name of your module. Make this short and should not contain any spaces. This must be unique to your module, so something like Team-xxxx-Module
is a good choice.
getModuleVersion()
will return the version of your module. This should be semantic, i.e. 0.0.1
. This enables the Module Developer to debug exactly what version they should be targeting if you run into a bug.
prestart()
and start()
methods can also be overloaded for Robot setup just like you would in a WPILib RobotBase class.
IterativeModule is the most commonly used module baseclass, and provides much the same functionality as WPILib's IterativeRobot
class. Methods for state operations are very simple, stateInit()
and statePeriodic()
, where state is the name of the robot state, for example: autonomousInit()
and teleopPeriodic()
. You can overload these methods like so:
@Override
public void autonomousInit() {
// Do your Autonomous Start code here!
}
Unlike WPILib, you don't have to overload what you don't need to, so feel free to use what you need.
There's a minor difference between WPILib and Toast in the way that Motors, DIO and AIO are created. While strongly recommended, it's not required.
Motors, DIO and AIO are handled by the Registrar
class. The Registrar avoids port conflicts by making sure PWM, DIO and AIO ports are constructed only once, removing the chances of an Allocation Exception when created by multiple modules. Using the Registrar is very simple, as seen below:
// Demonstration
Talon myTalon = Registrar.talon(0);
Talon myOtherTalon = Registrar.talon(0);
Talon anotherTalon = Registrar.talon(1);
myTalon == myOtherTalon // True
myTalon == anotherTalon // False
// Digital IO
DigitalOutput digOut = Registrar.digitalOutput(0);
DigitalInput digIn = Registrar.digitalInput(1);
// Analog IO
AnalogOutput anOut = Registrar.analogOutput(0);
AnalogInput anIn = Registrar.analogInput(1);
// Motors
Talon talon = Registrar.talon(0);
TalonSRX talonSRX = Registrar.talonSRX(1); // PWM Talon SRX
CANTalon canTalon = Registrar.canTalon(0); // CAN Talon SRX
Jaguar jaguar = Registrar.jaguar(2); // PWM Jaguar
CANJaguar canJaguar = Registrar.canJaguar(1); // CAN Jaguar
Victor victor = Registrar.victor(3);
VictorSP victorSP = Registrar.victorSP(4);
Hooks are provided for all supported motor controllers, digital IO and analog IO.
If you require it, you may register your own types on the Registrar using the underlying Registrar
instance in the static fields of the class.
MyCustomDIO myCustomDIO = Registrar.dioRegistrar.fetch(port, MyCustomDIO.class, () -> { new MyCustomDIO(port); });
States are Toast's way of dealing with the mode the Robot is in. These modes are known as Disabled, Autonomous, Teleop and Test. Toast provides an easy way to listen to State changes and react accordingly. This is done through the StateListener interfaces. A simple example is seen below:
StateTracker.addTicker(new StateListener.Ticker() {
@Override
public void tickState(RobotState state) {
// Yay, I'm ticking now!
}
});
StateTracker.addTransition(new StateListener.Transition() {
@Override
public void transitionState(RobotState state, RobotState oldState) {
// Yay, I'm in a new state!
}
});
or, using the preferred Lambda syntax:
StateTracker.addTicker(state -> {
// Yay, I'm ticking now!
});
StateTracker.addTransition((newstate, oldstate) -> {
// Yay, new state!
});
Each module should create its own Logger instance. A Logger is Toast's solution to making a clean, and easy to read console log that is saved to file. If you're using HotPlate, this logger is setup for you, however, creating one yourself is really simple:
Logger myLogger = new Logger("MyModule", Logger.ATTR_DEFAULT);
myLogger.info("Hello World!");
myLogger.warn("Oh No!");
myLogger.error("I'm broken :c");
myLogger.severe("I'm dead x_x");
Each logger has an Attributes field on the constructor. These attributes define what the logger outputs, for example, Colors, Time and Thread name. By default, all 3 of these options are enabled, but you can make your own combination like so:
// Time Only
Logger myLogger = new Logger("MyModule", Logger.ATTR_TIME);
// Time and Color
Logger myLogger = new Logger("MyModule", Logger.ATTR_TIME | Logger.ATTR_COLOR);
// Color and Thread
Logger myLogger = new Logger("MyModule", Logger.ATTR_COLOR | Logger.ATTR_THREAD);
The Toast Heartbeat system is a way of scheduling a task to execute once every 100 milliseconds, without being delayed. The difference between Heartbeats will always be 100ms, and if processing takes longer, the system will 'skip a beat' to keep everything on a constant clock. To register a listener to the Heartbeat System, do the following:
Heartbeat.add(new HeartbeatListener() {
@Override
public void onHeartbeat(int skipped_beats) {
MyModule.log.info("duh-dunk!");
}
});
or, using the preferred Lambda syntax:
Heartbeat.add(skipped -> {
MyModule.log.info("duh-dunk!");
});
Commands are Toast's way of providing an interactive interface directly to the Program. Commands are executed in the CommandLine (stdin), or over the network through the CommandDelegate. To add your own, custom command, you can do so like the following:
class MyCommand extends AbstractCommand implements IHelpable { // IHelpable is optional, but provides a help message
public String getCommandName() {
return "mycommand";
}
public void invokeCommand(int argLength, String[] args, String fullCommand) {
MyModule.log.info("You told me to: " + Arrays.toString(args));
}
public String getHelp() {
return "Prints out all the arguments you give me! Like a parrot!";
}
}
CommandBus.registerCommand(new MyCommand());
If your module requires the Filesystem (loading / writing to files), the Storage
class is your new best friend.
If you need to read/write from a single file, you can call Storage.highest_priority(filename)
to get back a File to write to. If you want to write to all devices (and respect the device's module settings), you can instead call Storage.USB_Module(filename)
method. Both of these methods can optionally take a callback as a second argument if you want to get the File, whether it is a USB or not, and if it is, a MassStorageDevice instance.
In most cases, Storage.highest_priority
is the method you want to use.
The Toast Asynchronous Bus is used to execute what are known as "Background Tasks". Background Tasks are executed in a new Thread (i.e. in the Background). Background Tasks are executed when they can be, so they aren't necessarily executed 'straight away'. This is usually used for calculations.
To submit a task, do the following:
Async.schedule(new AsyncTask() {
@Override
public void runTask() {
// Your Task is run here
}
});
or, using the preferred Lambda syntax:
Async.schedule(() -> {
// Your Task is run here
});
If you wish to wait for a task to finish, you can do so:
Future fut = Async.schedule(() -> {});
fut.get();
Toast comes with a basic way of loading dependencies. This is done through the 'Branch' model.
The @Branch annotation is added to the top of your Robot Base Module Class, and contains a branch (class name), a dependency (module name or class name) and a method.
@Branch(branch = "my.package.MyBranch", dependency = "SomeOtherModule", method = "some_other_module")
@Branch(branch = "my.package.MyOtherBranch", dependency = "my.other.package.AClass", method = "a_class")
public class MyModule extends IterativeModule { ... }
The branch
property is a class in your module that will be loaded. The method
property is the name of the method to call on this class.
The dependency
property is the name of the Module or Class Name (with package) that you want to depend on.
In the above example, we are calling my.package.MyBranch.some_other_module()
if the module SomeOtherModule
is loaded. We are also calling my.package.MyOtherBranch.a_class()
if the class my.other.package.AClass
is loaded.
If the dependencies cannot be found, the classes and methods simply will not be called.