Paradigms - Murray-Bridge-Bunyips/BunyipsLib GitHub Wiki

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.

OpMode

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.

Robot subsystems

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 explained in more detail in Task interactions with subsystems.

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

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.

Important

The Task is a supertype of the RoadRunner Action. Despite this, it is not recommended to use the Action by itself and the runner for it, the Actions.runBlocking implementation. You should use the Task class and derivatives where possible.

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
        named("My Task");
        timeout(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 definition, an anonymous class (of Task) should generally be used.

An anonymous class instance is simpler when tracking a lot of state, however, if you only need to track a simple object the sharedRef field is accessible by the DynamicTask supplier variants (e.g. .init((t) -> t.sharedRef)) and is a LateInitCell of generic type Object that you can use to store some simple state or object that you can cast.

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.

Tasks can be run either manually via their execute() or run() method, via Tasks which provides static utilities for running these tasks, via AutonomousBunyipsOpMode, or via a Scheduler which may or may not be affixed to CommandBasedBunyipsOpMode.

Tasks can also be dynamically edited via the DynamicTask, by calling mutate() on them. You can add functions to run after defined ones in the current task, or you can replace functionality and add to the task as you please.

Task task = new SomeTask();
task.mutate().addInit((t) -> Dbg.log("hello")); // Now an instance of DynamicTask with an added init block

Groups

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 extend 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 = Task.task().timeout(Seconds.of(3)).onFinish(() -> telemetry.log("done 1!"));
Task taskTwo = Task.task().timeout(Seconds.of(5)).onFinish(() -> telemetry.log("done 2!"));

Parallel task group

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
Task.par(taskOne, taskTwo); // Another alternative

// <3s delay>
// done 1!
// <2s delay>
// done 2!

Sequential task group

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
Task.seq(taskOne, taskTwo); // Another alternative

// <3s delay>
// done 1!
// <5s delay>
// done 2!

Incrementing task group

An incrementing task group is similar to a sequential task group, but the process of moving from one task to the next is not executed automatically.

An incrementing task group only increments when the task group is initialised (or reinitialised), or a manual call to increment() is made. Increments wrap around so this task group is good for toggles.

new IncrementingTaskGroup(
   taskOne,
   taskTwo
);
taskOne.next(taskTwo); // Alternative construction
Task.inc(taskOne, taskTwo); // Another alternative

// <3s delay>
// done 1!
// <task will report as done, on the next initialisation however>
// <5s delay>
// done 2!
// <task will report as done, on the next initialisation however>
// <repeats>
// <3s delay>
// done 1!

Deadline task group

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
Task.ddl(taskOne, taskTwo); // Another alternative

// <3s delay>
// done 1!
// done 2!

Race task group

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
Task.rce(taskOne, taskTwo); // Another alternative

// <3s delay>
// done 1!
// done 2!
// <note: if task 2 finished before task 1, task 1 would be finished when task 2 is finished>

Deferred task

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 as DynamicTask (not to be confused with the modern DynamicTask!)

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() construct 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
// Task b = Task.defer(this::reportCurrentTimeTask); // Alternative

// delay 1s
a.execute(); // time = 1s
// delay 1s
a.execute(); // time = 2s
b.execute(); // time = 0s
// delay 1s
a.execute(); // time = 3s
b.execute(); // 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.

Task interactions with subsystems

Tasks often have to access subsystems to carry out the actions they are defined to do.

A problem quickly arises if tasks are to be set in a scheduled manner, where two different tasks try to access a single subsystem. This causes erratic hardware access as the actuators attempt to accept commands from two sources.

The dependency system is integrated into every task and paired with execution of a task via the execute() method.

Each BunyipsSubsystem is given one task which it may "own" at any time, as well as a default task to run continuously when it is not given a task to "own". As the subsystem updates through update(), the task attached to it will also be updated with one cycle of run().

Important

The execute() method is actually a proxy for run(), which will attempt to schedule the task onto the subsystem for the subsystem to call run() when possible!

To assign a "dependency" to a task, simply call task.on(BunyipsSubsystem, boolean), where the first argument is the subsystem and the second argument is conflict behaviour. If the boolean is set to true, any existing task currently running on the subsystem will be cancelled with finishNow(), and the new task will take the spot. If false, which is the default, the new task will not be scheduled and the currently scheduled task will continue execution.

As mentioned previously for integrated tasks on subsystems via the tasks field, all of these tasks internally call on(this, true), meaning they can be used freely and know which subsystem to run on. Setting a task as a default task is equivalent to running on(this, false).

This is because default tasks do not count as conflicts and will always be overridden.

Important

Task groups/task compositions cannot be assigned to subsystems. This is because they will execute independently and recursively call execute(), assigning the internal tasks to their respective subsystem if possible.

A Driver Station warning will be issued if on() is called for a composite task, which includes all task groups, DeferredTask, and others which produce a task from a task.

When a subsystem is not running a scheduled task, the default task will be running. A default task is like any other task but is performing some continuous action such as holding a position.

Warning

Default tasks, when assigned, should never finish! This means if poll() is called on the task, it should never return true. A fatal exception will be thrown otherwise.

To assign a default task to a subsystem, either call subsystem.setDefaultTask(...) or task.setAsDefaultTask().

Caution

task.setAsDefaultTask() cannot be called if there is no set dependency on the task as set by on! However, this utility method is handy when assigning scheduler binds, as often using the integrated tasks means on has been automatically called for you.

robot.subsystem.setDefaultTask(robot.subsystem.tasks.someTask()); // This works but repeats robot.subsystem which can get mixed up
robot.subsystems.tasks.someTask().setAsDefaultTask(); // This is recommended, as this task is an internal task that calls `on(this, true)` 

Scheduling

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.

Iterative

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.

Commanded

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

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.

Geometry

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.

Reference frame

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.

Vector

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

Pose

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

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

Variants such as the PoseVelocity2d and Dual mode geometry objects appear as per RoadRunner.

PoseVelocity2d

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).

Twist2d

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.

Dual objects

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.

Done with theory?

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.

⚠️ **GitHub.com Fallback** ⚠️