-
Notifications
You must be signed in to change notification settings - Fork 0
Guide to Making Our First Robot
Welcome! In this guide we will be making our first FlashLib robot. We want be talking about mechanics and electronics, but rather only software. You're free to build an actual robot, but the purpose of this guide is to familiarize ourselves with the structure and components of FlashLib robots.
We won't be focusing much about the development environment used, since it doesn't matter much. However, we will be showing some images and information about our preferred environment, in order to clarify some stuff that might not be obvious. The environment of our choice is a gradle with a gradle wrapper and using IntelliJ IDEA. The Java version can be of your choosing, as long as it is compatible with 1.8 binaries. For the purposes of this guide we'll be using 1.8, although it should have much (if any) affect and thus won't be apperent.
We won't be explaining how to setup a gradle project, but rather skip ahead.
This should be the look of your project if you're using gradle. At the moment it is empty. Let's start by setting up the build script. Open up the build.gradle
file, which should be empty (if not, clear it).
We are going to be using the application plugin which provides an simple setup for running Java applications. Add a plugins DSL statement to use the plugin:
plugins {
id 'application'
}
We also need to setup the application configuration, which includes the main class. We don't have main class yet, but we would make it in accordance to what we define here:
application {
mainClassName = 'robot.Main'
}
Next we need to define FlashLib as a dependency of the project. We will start by defining the project repositories which are where dependencies will be retrieved from, and then add the dependency:
repositories {
mavenCentral()
}
dependencies {
implementation group: "com.flash3388.flashlib", name: "flashlib.core.robot", version: "$version"
}
Where $version
is the FlashLib version you wish to use.
This should be the end product of your build.gradle
(we've changed the order a bit):
plugins {
id 'application'
}
repositories {
mavenCentral()
}
dependencies {
implementation group: "com.flash3388.flashlib", name: "flashlib.core.robot", version: "$version"
}
application {
mainClassName = 'robot.Main'
}
We'll create the package which will contain the robot code. Let's call it robot
.
Create the robot
package by right clicking the java
folder -> New
-> Package
and name it robot
:
Now we add the main class for the project. We will make it under robot
package in a class named Main
(so robot.Main
).
Create the main class by right clicking the robot
package -> New
-> Java Class
and name it Main
:
This will result in an empty class. Thanks to how we configured gradle (setting mainClassName
) this class will be our main class. We now need to implement the main method which will start running the robot.
package robot;
public class Main {
public static void main(String[] args) {
}
}
Let's leave it for now, we'll come back to this class and finish it.
The RobotControl
of a robot is responsible for providing different control components, which are necessary in order to operate the robot.
Unfortunately because we don't have a physical robot, it is difficult to fully implement RobotControl
, since the components can be platform dependent. Because of this, we will be using stub components for parts which depend on the platform used; they can be easily substituted later.
Create a new class called MyRobotControl
that implements the RobotControl
interface:
public class MyRobotControl implements RobotControl {
@Override
public Supplier<? extends RobotMode> getModeSupplier() {
return null;
}
@Override
public IoInterface getIoInterface() {
return null;
}
@Override
public HidInterface getHidInterface() {
return null;
}
@Override
public Scheduler getScheduler() {
return null;
}
@Override
public Clock getClock() {
return null;
}
@Override
public Logger getLogger() {
return null;
}
@Override
public void registerCloseables(Collection<? extends AutoCloseable> closeables) {
}
}
There are a bunch of methods we need to implement. Let's create an instance variable for all the components:
public class MyRobotControl implements RobotControl {
private final Supplier<? extends RobotMode> mRobotModeSupplier;
private final IoInterface mIoInterface;
private final HidInterface mHidInterface;
private final Scheduler mScheduler;
private final Clock mClock;
private final Logger mLogger;
private final ResourceHolder mResourceHolder;
...
The components are:
-
mRobotModeSupplier
: supplies the operation modes of the robot. -
mIoInterface
: allows interfacing with the hardware of the local platform, in order to control electronic components. -
mHidInterface
: allows interfacing with controllers and other human interface devices, providing human control. -
mScheduler
: the execution component for action-based robots. -
Clock
: provides timestamp information. Used by other components. -
Logger
: the robot log. -
ResourceHolder
: an helper class for holding resources used by the robot.
We now need to provide values for our variables. We'll do this via the constructor. The basic constructor should receive Logger
and ResourceHolder
and create the rest. But this can be modified if needed:
public MyRobotControl(Logger logger, ResourceHolder resourceHolder) {
mLogger = logger;
mResourceHolder = resourceHolder;
mClock = RobotFactory.newDefaultClock();
mRobotModeSupplier = new StaticRobotModeSupplier(RobotMode.DISABLED);
mIoInterface = new IoInterface.Stub();
mHidInterface = new HidInterface.Stub();
mScheduler = RobotFactory.newDefaultScheduler(mClock, mLogger);
}
The Clock
and Scheduler
implementations are easily provided from FlashLib and are fully functional.
The RobotModeSupplier
implementation used here is one that uses only a single mode: RobotMode.DISABLED
. Normally we would want something that can be modified remotely, but there is no built-in implementation which provides that.
The IoInterface
is platform-dependent since it interacts of hardware. Because of that, it's a stub.
The HidInterface
should normally be controlled remotely, but there is no such built-in implementation. Thus, we use a stub here.
With the components created, we can implement the methods now:
public class MyRobotControl implements RobotControl {
private final Supplier<? extends RobotMode> mRobotModeSupplier;
private final IoInterface mIoInterface;
private final HidInterface mHidInterface;
private final Scheduler mScheduler;
private final Clock mClock;
private final Logger mLogger;
private final ResourceHolder mResourceHolder;
public MyRobotControl(Logger logger, ResourceHolder resourceHolder) {
mLogger = logger;
mResourceHolder = resourceHolder;
mClock = RobotFactory.newDefaultClock();
mRobotModeSupplier = new StaticRobotModeSupplier(RobotMode.DISABLED);
mIoInterface = new IoInterface.Stub();
mHidInterface = new HidInterface.Stub();
mScheduler = RobotFactory.newDefaultScheduler(mClock, mLogger);
}
@Override
public Supplier<? extends RobotMode> getModeSupplier() {
return mRobotModeSupplier;
}
@Override
public IoInterface getIoInterface() {
return mIoInterface;
}
@Override
public HidInterface getHidInterface() {
return mHidInterface;
}
@Override
public Scheduler getScheduler() {
return mScheduler;
}
@Override
public Clock getClock() {
return mClock;
}
@Override
public Logger getLogger() {
return mLogger;
}
@Override
public void registerCloseables(Collection<? extends AutoCloseable> closeables) {
mResourceHolder.add(closeables);
}
}
It's not a complex class, but a crucial one. Changing the implementation of components is simple, as all that is required is to change the instance created in the constructor to the wanted one.
Not to be confused with the Main
class, the Robot Class (or Robot Main Class) is the class responsible for defining and running the robot. It is based on a Robot Base, which defines a running context for the robot.
We will start by creating a class MyRobot
under robot
package:
package robot;
public class MyRobot {
}
We are going to use the LoopingRobotBase
which provides an iterative flow based on the robot mode. To use this base we will implement the IterativeRobot
interface - public class MyRobot implements IterativeRobot
and add a single constructor, which receives RobotControl
(and possibly saves it in an instance variable, if wanted):
public class MyRobot implements IterativeRobot {
private final RobotControl mRobotControl;
public MyRobot(RobotControl robotControl) {
mRobotControl = robotControl;
}
@Override
public void disabledInit() {
}
@Override
public void disabledPeriodic() {
}
@Override
public void modeInit(RobotMode mode) {
}
@Override
public void modePeriodic(RobotMode mode) {
}
@Override
public void robotPeriodic() {
}
@Override
public void robotStop() {
}
}
With all our classes set-up, we can go back to main
and implement the code that will run the robot.
We will start by creating the Logger
for the robot:
Logger logger = new LoggerBuilder("robot")
.build();
You can add calls to the LoggerBuilder
methods in order to configure the logger, as described here.
With that, we call RobotMain.start
which will run our robot program. It receives information about our robot base and robot control in the form of RobotCreator
interface, which will come from the classes we created. We can do this simply with a lambda:
public class Main {
public static void main(String[] args) {
Logger logger = new LoggerBuilder("robot")
.build();
RobotMain.start((_logger, resourceHolder) -> {
RobotControl robotControl = new MyRobotControl(logger, resourceHolder);
RobotBase robotBase = new LoopingRobotBase(MyRobot::new);
return new RobotImplementation(robotControl, robotBase);
}, logger);
}
}
And with that our main
is complete.
With our project all setup, we can now actually run the code, but we won't do that just yet.
The next step in our robot are the subsystems. The robot subsystems are different mechanical or electronic components of the robot which have a specific job and are independent from others. Each subsystem can be comprised of a dozen different components which are all working together to achieve one task (or several).
FlashLib promotes a concept of System-Oriented Code, which is described further here, and you should read it before continuing.
With the understanding on that paradigm in mind, we can start making our subsystems. Since we don't have an actual physical robot available, we'll make one up.
Let's say that the drive system of our robot is as shown in the image. Our job is to implement the subsystem code for it. The system shown is a tank-like drive, which utilizes 4 motors - 2 for each side. It is considered tank-like because each side is operated separately and thus is moved separately.
We'll start by creating a new package under robot
called subsystems
, under which we'll create our subsystem classes. Like before, we'll right click the robot
package -> New -> Package and add .subsystems
so that the text box shows robot.subsystems
.
Now we need to create the class for the subsystem. We'll right click the new subsystems
package -> New -> Class and enter a name - DriveSystem
.
package robot.subsystems;
public class DriveSystem {
}
For a class to be a subsystem it must extend Subsystem
, which is what we'll do:
public class DriveSystem extends Subsystem {
}
Next we need to add properties (or instance variables) to the class. According to the System-Oriented design, those will be electronic components that are used to operate the system. In our system we have 4 electric motors, 2 per side connected to a gearbox. Each motor is controlled by some speed controller, so that brings us to 4 speed controllers - 2 for the right side, 2 for the left side:
public class DriveSystem extends Subsystem {
private final SpeedController mRight1;
private final SpeedController mRight2;
private final SpeedController mLeft1;
private final SpeedController mLeft2;
}
Since the speed controllers for each side will operate together (mRight1
+ mRight2
, mLeft1
+ mLeft2
) we can combine each 2 under a single SpeedController
variable and create with SpeedControllerGroup
(we'll how later):
public class DriveSystem extends Subsystem {
private final SpeedController mRight;
private final SpeedController mLeft;
}
We should receive our properties directly from the constructor, leaving the creation of the components up to the robot class:
public class DriveSystem extends Subsystem {
private final SpeedController mRight;
private final SpeedController mLeft;
public DriveSystem(SpeedController right, SpeedController left) {
mRight = right;
mLeft = left;
}
}
With our properties defined an initialized, let's move onward to the methods. In System-Oriented design, the methods represent simple operations that the components are capable of, like set speed, stop, etc. This is exactly what we're going to do. To help us choose the methods to export, in addition to providing some extra functionality; we can use system interfaces from FlashLib. These interfaces declare methods which should be provided by subsystems of different types. In our case, the system is a tank-like drive, which is represented by TankDrive
:
public class DriveSystem extends Subsystem implements TankDrive {
private final SpeedController mRight;
private final SpeedController mLeft;
public DriveSystem(SpeedController right, SpeedController left) {
mRight = right;
mLeft = left;
}
@Override
public void tankDrive(double right, double left) {
}
@Override
public void arcadeDrive(double moveValue, double rotateValue) {
}
@Override
public void stop() {
}
}
We received 3 methods to implement from the interface:
-
tankDrive
defines a tank-like control, where each side is directed separately. The values receives are precent VBus values for the motors. -
arcadeDrive
is a more complicated control for tank-like drives: it emulates an arcade joystick game, where a single joystick, with its Y and X values, is used to move the drive. -
stop
is a simple directive to stop the motion of the system, i.e. the motor.
Implementing tankDrive
and stop
is quite simple. However, arcadeDrive
is more challenging, so we'll leave it for later.
@Override
public void tankDrive(double right, double left) {
// since right and left parameters are percent vbus, just like what SpeedController.set expects, we can just pass it directly.
mRight.set(right);
mLeft.set(left);
}
@Override
public void stop() {
mRight.stop();
mLeft.stop();
}
To implement arcadeDrive
we'll use a helper class called DriveAlgorithms
which implements several algorithms for drive system control, arcadeDrive
among them. We'll care an instance of it at the class level and use it in arcadeDrive
:
public class DriveSystem extends Subsystem implements TankDrive {
private final SpeedController mRight;
private final SpeedController mLeft;
private final DriveAlgorithms mAlgorithms;
public DriveSystem(SpeedController right, SpeedController left) {
mRight = right;
mLeft = left;
mAlgorithms = new DriveAlgorithms(); // no need to receive it from robot
}
@Override
public void arcadeDrive(double moveValue, double rotateValue) {
TankDriveSpeed speed = mAlgorithms.arcadeDrive(moveValue, rotateValue);
// TankDrive interface provides a tankDrive(TankDriveSpeed) method with a default implementation that calls tankDrive(double, double). Thanks to that, we can simply pass speed to this method.
// This is one among several methods provided with default implementation by TankDrive.
tankDrive(speed);
}
The final product we get:
public class DriveSystem extends Subsystem implements TankDrive {
private final SpeedController mRight;
private final SpeedController mLeft;
private final DriveAlgorithms mAlgorithms;
public DriveSystem(SpeedController right, SpeedController left) {
mRight = right;
mLeft = left;
mAlgorithms = new DriveAlgorithms();
}
@Override
public void tankDrive(double right, double left) {
mRight.set(right);
mLeft.set(left);
}
@Override
public void arcadeDrive(double moveValue, double rotateValue) {
TankDriveSpeed speed = mAlgorithms.arcadeDrive(moveValue, rotateValue);
tankDrive(speed);
}
@Override
public void stop() {
mRight.stop();
mLeft.stop();
}
}
Now that the class is finished, we can use it in the robot class. Subsystem classes should only have one instance, which will be used throughout the code. They should be initialized in the robot init phase. Assuming we are using a LoopingRobotBase
and implementing IterativeRobot
:
public class Robot implements IterativeRobot {
private final DriveSystem mDriveSystem;
public Robot(RobotControl robotControl) {
mDriveSystem = new DriveSystem(rightMotors, leftMotors);
}
}
rightMotors
and leftMotors
are the speed controllers for the system. FlashLib recommends the use of a RobotMap
to store connection information. So we'll create a RobotMap
class under robot
package:
package robot;
public class RobotMap {
}
We'll add constants for each of our motors, as each has a connection. The data type is IoChannel
from flashlib.core.io
. The creation of those channels depends on the implementation we use, which is stub is our case, thus we will use stub channels:
public class RobotMap {
public static final IoChannel DRIVE_RIGHT1 = new IoChannel.Stub();
public static final IoChannel DRIVE_RIGHT2 = new IoChannel.Stub();
public static final IoChannel DRIVE_LEFT1 = new IoChannel.Stub();
public static final IoChannel DRIVE_LEFT2 = new IoChannel.Stub();
}
We'll use those to create our speed controllers. This depends on the specific speed controller device used for each motor, so for this example we'll use PwmTalonSrx
, which implements a PWM-connected TalonSRX. Creating one looks something like this:
RobotControl robotControl = ...
Pwm pwm = robotControl.newPwm(RobotMap.DRIVE_RIGHT1);
SpeedController speedController = new PwmTalonSrx(pwm);
public Robot(RobotControl robotControl) {
SpeedController right1 = new PwmTalonSrx(robotControl.newPwm(RobotMap.DRIVE_RIGHT1));
SpeedController right2 = new PwmTalonSrx(robotControl.newPwm(RobotMap.DRIVE_RIGHT2));
SpeedController rightMotors = new SpeedControllerGroup(right1, right2);
SpeedController left1 = new PwmTalonSrx(robotControl.newPwm(RobotMap.DRIVE_LEFT1));
SpeedController left2 = new PwmTalonSrx(robotControl.newPwm(RobotMap.DRIVE_LEFT2));
SpeedController leftMotors = new SpeedControllerGroup(left1, left2);
mDriveSystem = new DriveSystem(rightMotors, leftMotors);
}
TODO: CONTINUE WHEN I GET THE CHANGE.