-
Notifications
You must be signed in to change notification settings - Fork 0
Paradigms
A paradigm is a design pattern used to standardise operations. In BunyipsLib, several paradigms have been fused, created, and integrated to provide a high-level interface for programmers. The three main paradigms are the OpMode, Task system, and Subsystems. Other paradigms include geometry methods that are essential for using any type of drive subsystem in BunyipsLib.
The OpMode is an abstract class provided by the SDK which may be implemented to run user code. In BunyipsLib, the abstract class to use is a BunyipsOpMode
, which is an abstract extension of the OpMode
that provides various built-in features as discussed in other sections. This chain of inheritance is essential to understand when looking at OpMode interactions with subsystems and components.
The general structure of a BunyipsOpMode
contains init and active loop sections, which will run initialisation code once when the INIT button is pressed, and loop the active loop contents continuously after START is pressed.
import au.edu.sa.mbhs.studentrobotics.bunyipslib.BunyipsOpMode;
@TeleOp(name = "My TeleOp!")
public class MyOpMode extends BunyipsOpMode {
@Override
protected void onInit() {
// Runs once when the INIT button is pressed
}
@Override
protected void activeLoop() {
// Runs continuously after START is pressed
}
}
For Autonomous, a separate extension is used called AutonomousBunyipsOpMode
, which extends BunyipsOpMode
but instead implemements a queue for the Task system, as detailed below. Further information regarding OpModes is covered in the Creating OpModes section, which you can review later.
A flowchart is provided to demonstrate the overhead connection between components from the SDK and components of BunyipsLib.
Subsystems are the basic unit of robot organisation in the task-based paradigm. They can also be used without the paradigm, offering a multi-paradigm approach for different skill level programmers. A subsystem is an abstraction for a collection of robot hardware that operates together as a unit.
Subsystems encapsulate this hardware, “hiding” it from the rest of the robot code (e.g. tasks and public methods) and restricting access to it except through the subsystem’s public methods. Restricting the access in this way provides a single convenient place for code that might otherwise be duplicated in multiple places (such as scaling motor outputs or checking limit switches) if the subsystem internals were exposed. It also allows changes to the specific details of how the subsystem works (the “implementation”) to be isolated from the rest of robot code, making it far easier to make substantial changes if/when the design constraints change.
Subsystems can be associated with “default tasks” that will be automatically scheduled when no other task is currently using the subsystem.
This is useful for continuous “background” actions such as controlling the robot drive, or keeping an arm held at a setpoint. Similar functionality can be achieved in the subsystem’s periodic()
method, which is run once per run of the subsystem's update()
method; programmers should try to be consistent within their codebase about which functionality is achieved through either of these methods. Subsystems are represented in BunyipsLib by the BunyipsSubsystem interface.
Note
Various subsystems are built into BunyipsLib - some of which you may have seen before in the robot configuration page! Review the Subsystems article before beginning the process of determining which subsystems you wish to create or use.
A custom subsystem can be created by extending BunyipsSubsystem
:
import au.edu.sa.mbhs.studentrobotics.bunyipslib.BunyipsSubsystem;
public class MySubsystem extends BunyipsSubsystem {
public MySubsystem() {
// Use this constructor to get references to your hardware and store them as fields
}
// Expose public methods to control the subsystem here
@Override
protected void periodic() {
// Runs repeatedly while the subsystem is active
// This is where hardware propagation should be done
}
}
Caution
A built-in safety feature of BunyipsLib is by encapsulating hardware, the state of hardware will always be known. Thus, it is important to know if you are creating your own subsystems to only propagate changes to the hardware inside of the periodic()
method, otherwise functionality such as disable()
may continue dangerous calls to hardware and cause unpredictable behaviours.
Tasks are actions the robot can perform. This paradigm is known as the command-based paradigm and is used in BunyipsLib as the core of the Autonomous actions system, and can optionally be used in TeleOp as part of the command-based scheduler.
Tasks are small snippets of code designed to carry out one action, while holding state to whether the task is finished. These tasks can be run in a polling fashion to allow for multiple tasks to occur at the same time. Tasks may be scheduled directly or attached to subsystems to run in the context of the Scheduler
class.
To make a task, extend Task
, or use one of the utility tasks found in the tasks.bases
package to compose one.
import static au.edu.sa.mbhs.studentrobotics.bunyipslib.external.units.Units.Seconds;
import au.edu.sa.mbhs.studentrobotics.bunyipslib.tasks.bases.Task;
public class MyTask extends Task {
public MyTask() {
// Constructor for the task, use this to get references to your hardware and store them as fields
// You can also access the methods from Task here, to set timeouts and names
withName("My Task");
setTimeout(Seconds.of(10));
}
@Override
protected void periodic() {
// Runs repeatedly while the task is active
}
@Override
protected boolean isTaskFinished() {
// Return here true if the task should finish, timeout is monitored in the background
return false;
}
// Other defaulted overrides include onFinish(), onInterrupt(), and init() which can be used to hook into further task scheduling activity
}
You can also choose to use a builder pattern to construct tasks without a new class - using the DynamicTask
:
DynamicTask task = Task.task() // by default, builder methods that are not called are implicitly no-op or returns false, as shown
.init(() -> {})
.periodic(() -> {})
.isFinished(() -> false);
.onFinish(() -> {})
.onInterrupt(() -> {})
.onReset(() -> {});
Note
To track state in a DynamicTask
without creating a new class file, an anonymous class (of Task
) should be used. You may choose to use a Reference
to track state too, however, an anonymous class may be simpler.
Important
Many of the built-in BunyipsLib subsystems contain tasks that can be used directly through their tasks
field. The tasks
package in BunyipsLib also defines various tasks that can be used by default for many different purposes. Subsystems and tasks work hand-in-hand.
Individual tasks are capable of accomplishing a large variety of robot functions, but the simple three-state format can quickly become cumbersome when more advanced functionality requiring extended sequences of robot functions or coordination of multiple robot subsystems is required. In order to accomplish this, users are encouraged to use the powerful task group functionality included in BunyipsLib.
As the name suggests, task groups are combinations of multiple tasks. The act of combining multiple objects (such as tasks) into a bigger object is known as composition. Task groups compose multiple tasks into a composite task. This allows code to be kept much cleaner and simpler, as the individual component tasks may be written independently of the code that combines them, greatly reducing the amount of complexity at any given step of the process.
Most importantly, however, task groups are themselves tasks - they implement Task
. This allows task groups to be recursively composed - that is, a task group may contain other task groups as components.
Since the subsystem task attachment system is designed to hold a one-to-one relationship, Task groups cannot be attached to subsystems. The tasks that internally compose a Task group will instead be attached to subsystems as they are executed, as per the implementation of the Scheduler. Users must take care in commanding the same subsystem to perform multiple tasks.
For the below examples, consider:
Task taskOne = new RunForTask(Seconds.of(3), () -> {}, () -> telemetry.log("done 1!"));
Task taskTwo = new RunForTask(Seconds.of(5), () -> {}, () -> telemetry.log("done 2!"));
A parallel task group runs all the tasks inside of it at the same time, where the group will finish when all the internally composed tasks are finished. The timeout of this task group is represented by the maximum timeout of all the tasks.
new ParallelTaskGroup(
taskOne,
taskTwo
);
taskOne.with(taskTwo); // Alternative construction
// <3s delay>
// done 1!
// <2s delay>
// done 2!
A sequential task group runs through each task inside of it one by one, until every task is finished. It is important to consider if a task has a finish condition before allocating it into a SequentialTaskGroup, as it may stall the group if a task is not implemented to finish. The timeout of this task group is represented by the sum of all task timeouts.
new SequentialTaskGroup(
taskOne,
taskTwo
);
taskOne.then(taskTwo); // Alternative construction
// <3s delay>
// done 1!
// <5s delay>
// done 2!
A deadline task group is similar to a parallel task group, but will early finish all the tasks in the group when the first task in the group finishes.
new DeadlineTaskGroup(
taskOne,
taskTwo
);
taskOne.during(taskTwo); // Alternative construction
// <3s delay>
// done 1!
// done 2!
A race task group is similar to a parallel task group, but will early finish all the tasks in the group if any task in the group finishes.
new RaceTaskGroup(
taskOne,
taskTwo
);
taskOne.race(taskTwo); // Alternative construction
// <3s delay>
// done 1!
// done 2!
// <note: if task 2 finished before task 1, task 1 would be finished when task 2 is finished>
While not a task group in the conventional sense, the DeferredTask
is still a composed task. The DeferredTask takes in a lambda function that returns a task, effectively a reference to building a task, and then executes the construction process when the task is executed.
Pre BunyipsLib v6.1.0,
DeferredTask
was known asDynamicTask
(not to be confused with the modernDynamicTask
!)
Tip
AutonomousBunyipsOpMode
supports the addition of DeferredTasks easily with a variant to the add()
method called defer()
Deferred tasks are useful for tasks that rely on runtime conditions to function properly.
// let reportCurrentTimeTask() be a task that returns the delta time of the task runtime since construction
Task a = reportCurrentTimeTask(); // time = 0s
Task b = new DeferredTask(this::reportCurrentTimeTask); // passing a reference to the task instead, time is not defined yet
// delay 1s
a.run(); // time = 1s
// delay 1s
a.run(); // time = 2s
b.run(); // time = 0s
// delay 1s
a.run(); // time = 3s
b.run(); // time = 1s
Several other utility methods exist on the Task
class to compose the task to include wait durations (after
), minimum execution duration (forAtLeast
), and other useful compositions. Review the Task
class methods and accompanying Javadoc to see options.
BunyipsLib exposes three primary OpMode-related paradigms that can be used to construct OpModes. These paradigms link with other elements of the library, including the alternative base OpMode types which support a contextual environment for Task
instance to run.
The simplest and most naive implementation is the iterative paradigm. Commands are sent in a procedural manner according to an active loop, and update calls execute the new state on the actual hardware. This execution cycle is shown below, and is very simple. The iterative implementation of subsystem event loops is the default throughout BunyipsLib (present in general use of subsystems and via BunyipsOpMode
).
It is made with guarantee that without a call to update()
, no hardware changes will propagate.
The command-based OpMode is an extension of the iterative paradigm. A scheduler automatically handles the update cycle, and commanding methods are handled by triggering events. The commanded implementation is offered through the Scheduler
class, alternatively built-in if you use the CommandBasedBunyipsOpMode
.
Bindings are defined once per OpMode, along with tasks to accomplish when these bindings are met. These tasks are executed by the Scheduler
on the desired subsystem, and the update()
call is handled internally.
Thoroughly review the Scheduler
API docs for the command binds that can be accomplished.
Autonomous operations are a subset of the command-based paradigm. A queue of tasks is scheduled in initialisation, then executed one by one until the queue is completed. This is similar to a commanded binding of a singular SequentialTaskGroup
, set to run instantly when the OpMode begins. The queued autonomous paradigm is available in the AutonomousBunyipsOpMode
OpMode.
Several utility classes offered from the RoadRunner library are used in various places to represent geometry on the robot. These classes are also used within BunyipsLib to centralise functionality and to only rely on one source of reference frame. Elementary knowledge of vectors is the only prerequisite.
Tip
The Geometry
util class has assistance methods for managing geometry objects, and the Cartesian
and Controls
classes handle conversion from Cartesian/gamepad vectors to Robot vectors.
The reference frame of the robot is based on the Robot Coordinate System, which is mapped as +X being forward, +Y being left, and +θ being anticlockwise. This reference frame is derived from being situated facing forward on the Unit Circle, and is used in all implementations of vector and pose operations across BunyipsLib. A figure is shown below mapping this relationship from a birds-eye view of a robot.
RoadRunner and BunyipsLib use the FIRST Tech Challenge Field Coordinate System.
The zero origin of the FIRST Tech Challenge coordinate system is the point in the center of the field, equidistant from all 4 perimeter walls (where the four center tiles meet). The origin point rests on the top surface of the floor mat.
Looking at the origin from the RED WALL, the X axis extends through the origin point and runs to the right and left, parallel with the RED WALL. The X axis values increase to the right.
Looking at the origin from the RED WALL, the Y axis extends through the origin point and runs out and in, perpendicular to the RED WALL. Increasing Y values run out (away) from the RED WALL.
A vector represents a position on the field. The unit used for vectors within BunyipsLib (and RoadRunner) is the inch. Several utility functions exist to interchange between inches and other units, as well as integrated unit utilities when building RoadRunner trajectories. Read more about the BunyipsLib unit system in the Utilities section.
Vector2d position = new Vector2d(30, 30); // Defines a position 30 inches from the origin forward and left
A pose is a vector with a heading, used to represent the robot's position and orientation at any time on a 2D plane.
The unit used for heading is the radian. You can use Math.toRadians()
to convert degrees to radians, or you can use the BunyipsLib unit system to convert between angular units.
Pose2d pose = new Pose2d(-30, -30, Math.PI / 2); // Defines a position 30 inches from the origin back, right, and rotated 90 degrees CCW
// Pose2d can also be constructed with a vector
Vector2d position = new Vector2d(30, 30); // Defines a position 30 inches from the origin forward and left
Pose2d vectorPose = new Pose2d(position, 3 * Math.PI / 2); // 30 in forward, 30 in left, 270 (or -90) degrees rotation
Note
The Rotation2d class is a more advanced geometry concept, but still important to know in order to rotate vectors.
The rotation utility used in poses is actually a shorthand for the Rotation2d
, which is a class that represents a rotation. This rotation is modeled by a complex number in the form a+bi, which is equivalent to a 2D vector.
Rotation2d oneEightyDegrees = Rotation2d.exp(Math.PI);
Rotation2d thirtyDegrees = Rotation2d.exp(Math.PI / 6);
Rotation2d multiplied = oneEightyDegrees.times(thirtyDegrees);
double rad = multiplied.toDouble(); // equal to 7π/6
Representing rotation as a vector is essential in calculating rotation matrices, one of the most common use cases being Field-Centric navigation. This is done by rotating the target vector by the inverted current heading of the robot, which will rotate the control vector to align with the field. A simple example of using Rotation2d on a vector is shown below:
Vector2d vec = new Vector2d(10, 0);
Vector2d rotated = Rotation2d.exp(Math.PI / 2).times(vec);
// rotated is now a Vector2d of (0, 10)
Note
Tasks like the HolonomicDriveTask handle Field-Centric rotation math for you, this is an example to show the uses of Rotation2d.
Variants such as the PoseVelocity2d
and Dual
mode geometry objects appear as per RoadRunner.
The most common variant is the PoseVelocity2d, which is used to control drivebases. A PoseVelocity2d is the velocity variant of the Pose2d. You can construct one with the utility method Geometry.vel(x, y, theta)
, or by calling new
on the object and passing in a linear velocity and angular velocity (in units inches/sec, and rads/sec).
A twist is a delta step in pose, used primarily in the Localizer
interface as a Dual object. A twist is represented by a line, and angle.
Each Geometry object mentioned here as a Dual variant, which simply means it stores more information regarding the pose, including derivatives up to an nth power. While rare, constructing Dual objects may be needed for constant value functions. To construct these objects, you can use the XDual.constant(X, n)
function, where X is the variant (Pose2d, etc., or it can be DualNum
), and n is the number of derivatives to fill. Constructing a dual object manually will require a parameter (respect to variable, usually Time
or Arclength
), and you can construct the required derivatives there.
Have a look at the robot configuration, then have a look at composing OpModes and using some of the built-in subsystems to control hardware.