Welcome to NextFTC

NextFTC is a simple but powerful library for FTC. It is a library for everyone, rookies and seasoned experts alike. It introduces command features similar to that of WPILib, the library used by nearly all FRC teams. With NextFTC, you can create cleaner, modular, and more efficient code.

Features

  • Easy to use: NextFTC doesn't require you to write commands as classes, like you do in FTCLib.
  • Gamepad binding: Easy to bind commands to gamepad buttons
  • Subsystems: Subsystems help you organize your code. Additionally, they help you prevent problems by making sure that no two commands that use the same subsystem ever run at once.
  • Commands: Commands are units of code that can be executed. Each command is a bunch of tiny steps. Commands can be grouped into command groups, which allow commands to run together, either sequentially or at the same time.
  • Premade Commands: You almost never need to write your own commands, as there are dozens of commands already made for you. Examples are running a motor to a position using a custom PID controller, following a path, or driving during TeleOp.
  • Pedro Pathing: NextFTC has built in integration with Pedro Pathing, an autonomous pathing library. Unlike Roadrunner, Pedro Pathing is faster, smoother, and easier to tune.

A note on these docs

NextFTC was written in Kotlin, a JVM programming language. You can use either Kotlin or Java, but there are some small things in Kotlin that make your life slightly easier. Each section that gives code examples will have tabs for both Kotlin and Java. It is recommended to choose just one language for your project, to avoid having to figure out how to make your code compatible with itself.

If you will be using Kotlin, you must configure Kotlin in your project. I recommend using the Kotlin Gradle Plugin version 1.9.25. (Kotlin comes preinstalled in the Quickstart)

Installation

The first step to using NextFTC is installing it. There are two ways to install it.

Method 1: Quickstart Repo

This method is still in development

Method 2: Manually using Gradle

Installing NextFTC using Gradle is fairly simple. This tutorial assumes you are starting from an unmodified (or minimally modified) FtcRobotController project.

Step 1: Add the repositories

Open your build.dependencies.gradle file. Inside, you should see two "blocks" of code. The top one is the repositories block. Add the following lines to it:

maven { url = "https://maven.rowanmcalpin.com/" }
maven { url = "https://maven.pedropathing.com/" } // Remove if you don't intend to use PedroPathing
maven { url = "https://maven.brott.dev/" } // Remove if you don't intend to use the FTC Dashboard (required if using PedroPathing) 

Step 2: Add the dependencies

Still in the build.dependencies.gradle file, go to the dependencies block. Add the following lines to the bottom:

implementation 'com.rowanmcalpin.nextftc:core:0.5.5-beta1'
implementation 'com.rowanmcalpin.nextftc:ftc:0.5.5-beta1'
implementation 'com.rowanmcalpin.nextftc:pedro:0.5.5-beta1' // Remove if you don't intend to use PedroPathing
implementation 'com.pedropathing:pedro:1.0.3' // Remove if you don't intend to use PedroPathing
implementation 'com.acmerobotics.dashboard:dashboard:0.4.16' // Remove if you don't intend to use the FTC Dashboard (required if using PedroPathing)

Step 3: Sync Gradle

Click the Sync Now button that appeared as a banner at the top of your Gradle file.

You're good to go!

Subsystems

Subsystems are an important feature of NextFTC. A Subsystem is a collection of hardware devices and Commands that all interact with a discrete aspect of the robot, such as a lift, claw, or arm.

Generally, the first step when programming a robot is to program your subystems. This guide will walk you through a couple example subsystems that are found on many FTC robots, in order to give you the tools to create your own that fit your needs exactly.

Lift Subsystem

A subsystem that is found on almost all FTC robots in most seasons is a linear slide, also known as a lift. Here, you will learn how to program your own lift Subsystem.

Step 1: Create your class

The first step to creating your Subsystem is setting up the structure for it. Subsystems should be created as objects, which are singleton classes. Here is the most basic structure, that can be copy+pasted to create all of your subsystems.

object MySubsystem: Subsystem() {

}

In this case, let's call our subsystem Lift.

Step 2: Create your motor

Next, we need to set up a motor to power our lift. This is easy to do using the MotorEx class. Let's start by creating a variable to store our motor. It should be of type MotorEx. We're using lateinit var here because we can't initialize our motor until the OpMode has been initialized, since the hardware map isn't created until an OpMode has been initialized.

lateinit var motor: MotorEx

We also need a Controller, since we want to move our motor. Let's use a PID controller. I recommend using a P value of 0.005 to start, and leaving I and D at zero. You should come back and tune it later, though.

val controller = PIDController(PIDCoefficients(0.005, 0.0, 0.0))

Next, we need a name. This is the name specified in the hardwareMap, so that NextFTC can find the motor. I called mine lift_motor, so that's the name I'm using. Use whatever name you've set in your configuration:

val name = "lift_motor"

Finally, the last step to setting up a motor is creating the instance in the initialize() function. Let's do that now:

override fun initialize() {
    motor = MotorEx(name)
}

That's all you need to do to create a motor in NextFTC! To recap:

  • Every motor needs to be stored in a variable
  • Every motor needs a Controller
  • Every motor needs a name
  • Every motor must be instantiated in the initialize function.

We're not quite done, though. We still need to create our first commands!

Step 3: Create commands

The last step when you create a Subsystem is to create the commands you'll be using. This process varies with each subsystem. Here, I'll walk you through creating three commands that each move the lift to a different height: toLow, toMiddle, and toHigh.

tip

It's recommended to create a variable to store each encoder position. However, that takes up more space, so I won't be doing that here.

To control our motor, we will be using the RunToPosition command. There are a few different ways to implement commands, but the cleanest and recommended way is using getter methods. We will create variables (of type Command) and return instances of classes whenever we reference those variables.

Let's create our first RunToPosition command:

val toLow: Command
        get() = RunToPosition(motor, // MOTOR TO MOVE
            0.0, // TARGET POSITION, IN TICKS
            controller, // CONTROLLER TO IMPLEMENT
            this) // IMPLEMENTED SUBSYSTEM

Note the last parameter: subsystem. This is what tells NextFTC which commands should be allowed to run at the same time. If it weren't set, toLow would be able to run at the same time as other commands that use the Lift subsystem -- so there would be multiple things fighting to set the motor's power. Generally, you just need to pass this as the subsystem -- there are exceptions with more complicated custom commands.

Pretty easy, right? Let's duplicate it and update our variable name and target position to create our other two commands:

val toMiddle: Command
        get() = RunToPosition(motor, // MOTOR TO MOVE
            500.0, // TARGET POSITION, IN TICKS
            controller, // CONTROLLER TO IMPLEMENT
            this) // IMPLEMENTED SUBSYSTEM

    val toHigh: Command
        get() = RunToPosition(motor, // MOTOR TO MOVE
            1200.0, // TARGET POSITION, IN TICKS
            controller, // CONTROLLER TO IMPLEMENT
            this) // IMPLEMENTED SUBSYSTEM

Final result

That's it! You've created your first Subsystem! Here is the final result:

object Lift: Subsystem() {
    lateinit var motor: MotorEx

    val controller = PIDController(PIDCoefficients(0.005, 0.0, 0.0))

    val name = "lift_motor"

    val toLow: Command
        get() = RunToPosition(motor, // MOTOR TO MOVE
            0.0, // TARGET POSITION, IN TICKS
            controller, // CONTROLLER TO IMPLEMENT
            this) // IMPLEMENTED SUBSYSTEM

    val toMiddle: Command
        get() = RunToPosition(motor, // MOTOR TO MOVE
            500.0, // TARGET POSITION, IN TICKS
            controller, // CONTROLLER TO IMPLEMENT
            this) // IMPLEMENTED SUBSYSTEM

    val toHigh: Command
        get() = RunToPosition(motor, // MOTOR TO MOVE
            1200.0, // TARGET POSITION, IN TICKS
            controller, // CONTROLLER TO IMPLEMENT
            this) // IMPLEMENTED SUBSYSTEM

    override fun initialize() {
        motor = MotorEx(name)
    }
}

Claw Subsystem

Another common subsystem in FTC is a claw. Generally, a claw is powered by one servo, generally with an open and a closed position.

This will assume you have already read the Lift Subsystem guide. Let's get started!

Step 1: Create your class

Just like with the Lift Subsystem, we need to start by creating our object:

object Claw: Subsystem() {

}

Step 2: Create your servo

Now, since we're using a servo, instead of a motor, let's create a servo variable. Just like our motor variable from the lift subystem, it needs to be a lateinit variable. Currently, there is no wrapper class for Servos, so you will be using the Qualcomm servo class.

lateinit var servo: Servo

Just like our motors, we also need a name for our servo. This is the name specified in the hardwareMap. I called mine claw_servo:

val name = "claw_servo"

Finally, we need to initialize our servo in the initialize() function. Because there is no wrapper class, you will need to access the hardwareMap yourself. The easiest way to do this is by using OpModeData.hardwareMap. NextFTC will automatically set the hardwareMap in each of your OpModes (as long as you extend NextFTCOpMode or PedroOpMode):

override fun initialize() {
    servo = OpModeData.hardwareMap.get(Servo::class.java, name)
}

To recap how you create servo-based Subsystems in NextFTC:

  • Create a variable to store your Servo instance
  • Create a variable to store the name
  • In initialize(), get the Servo instance from the hardwareMap using your name variable.

Step 3: Create commands

Programming servo commands is very easy in NextFTC. Just like your Lift, you will be creating variables that return instances of Commands.

tip

It's recommended to create a variable to store each servo position. However, that takes up more space, so I won't be doing that here.

For servos, the command you will be using is ServoToPosition. You will pass your servo, a target position, and your subsystem (just like the Lift):

val open: Command
    get() = ServoToPosition(servo, // SERVO TO MOVE
        0.1, // POSITION TO MOVE TO
        this)  // IMPLEMENTED SUBSYSTEM

Nice! Let's do the same with the close command:

val close: Command
    get() = ServoToPosition(servo, // SERVO TO MOVE
        0.2, // POSITION TO MOVE TO
        this) // IMPLEMENTED SUBSYSTEM

Final result

You've successfully created your claw subsystem! Here's the final result:

object Claw: Subsystem() {
    lateinit var servo: Servo

    val name = "claw_servo"

    val open: Command
        get() = ServoToPosition(servo, // SERVO TO MOVE
            0.9, // POSITION TO MOVE TO
            this)  // IMPLEMENTED SUBSYSTEM

    val close: Command
        get() = ServoToPosition(servo, // SERVO TO MOVE
            0.2, // POSITION TO MOVE TO
            this) // IMPLEMENTED SUBSYSTEM

    override fun initialize() {
        servo = OpModeData.hardwareMap.get(Servo::class.java, name)
    }
}

OpModes

Autonomous

Creating an autonomous in NextFTC is fairly straighforward. This page will walk you through creating an autonomous. I will not be covering usage of PedroPathing in this page; refer to the PedroPathing page for details on that.

This autonomous program will introduce you to some core features of NextFTC such as command groups, delays, and running the commands you created in your Subsystems.

Let's get started!

Step 1: Create your class

OpModes in NextFTC will extend one of two classes: NextFTCOpMode and PedroOpMode, depending on if you're using PedroPathing.

This example will use NextFTCOpMode. Refer to the PedroPathing page to learn how to convert a NextFTCOpMode to a PedroOpMode and incorporate PedroPathing.

That being said, here is the basic structure for every Autonomous OpMode:

@Autonomous(name = "NextFTC Autonomous Program Kotlin")
class AutonomousProgram: NextFTCOpMode() {

}

That's not all, though. We want our autonomous program to use the Lift and Claw subsystems we created in the subsystems guide. To do that, we need to add them into the constructor of NextFTCOpMode:

class AutonomousProgram: NextFTCOpMode(Claw, Lift) {

This will tell NextFTC that we will be using those subystems in this OpMode, and will initialize them accordingly.

Step 2: Creating a routine

You've already learned how to create individual commands, such as motor and servo movements. Now, it's time to group them together into useful behaviors. This is where CommandGroups and routines come into play. Before we can create our own, we first need to understand how commands are run behind the scenes.

The CommandManager stores an internal list of actively running commands. It goes through each command and calls its update() function every single loop. It also determines which commands to cancel, handles subsystem conflicts, and offers additional functionality as well. The important thing to note is that all commands that are directly stored by the CommandManager run simultaneously. Knowing that, you may be wondering why ParallelGroups exist, if you can just schedule commands directly. Trust me, we'll get there. Before that, we need to understand what a SequentialGroup does.

A SequentialGroup stores a collection of commands and runs them in a row. Instead of scheduling all of its children at once, it schedules the first one, and then waits to schedule the next one until the first has completed, then continues scheduling them one by one until they've all completed. If you think about it a little bit, it may become apparent what a ParallelGroup is for. If you are using a SequentialGroup, you may have things you want to happen simultaneously within that group. For example:

  1. Drive to a location
  2. Simultaneously raise a lift and rotate an arm
  3. Open a claw
  4. Simultaneously lower the lift, reset the arm, and drive to a different location

This could only be accomplished using ParallelGroups in conjunction with SequentialGroups.

Now that we know what CommandGroups do, let's learn how to create them. For this example, we will create a routine that does the following:

  1. Raise the lift to the high position
  2. Simultaneously open the claw and move the lift to the middle position
  3. Wait for half a second
  4. Simultaneously close the claw and move the lift to the low position

This routine is unlikely to actually be useful in an autonomous program, but it will introduce you to the mental processes behind creating commands, and will give you the tools you need to create your own. Let's go ahead and create our command variable.

val firstRoutine: Command
    get() = SequentialGroup()

I've made an empty SequentialGroup here, for demonstration purposes.

caution

Do not attempt to use empty SequentialGroups in your code. They will cause errors that break your OpMode. If you need a placeholder, use a NullCommand.

The above snippet is incomplete and that's why it appears to create an empty SequentialGroup. That won't work in practice.

As mentioned above, we need to put something in our sequential group in order to avoid errors. In our list, we said the first thing we want to do is raise the lift to the high position. We can add the Lift.toHigh command into our group very easily:

val firstRoutine: Command
    get() = SequentialGroup(
        Lift.toHigh
    )

The next thing we wanted to do is simultaneously open the claw and move the lift to the middle position. To add another command to the group, add a comma at the end of your last item (in this case, Lift.toHigh) and add the next command on a new line (before the close parenthesis). In this case, we want to add a ParallelGroup because we want to do things simultaneously next.

val firstRoutine: Command
    get() = SequentialGroup(
        Lift.toHigh,
        ParallelGroup()
    )

Just like with SequentialGroups, you shouldn't create empty ParallelGroups (although it won't cause an error like SequentialGroups do). Let's populate it with our Lift.toMiddle and Claw.close commands:

val firstRoutine: Command
    get() = SequentialGroup(
        Lift.toHigh,
        ParallelGroup(
            Lift.toMiddle,
            Claw.close
        )
    )

Since command groups are also just commands, we can just continue to add commands after the ParallelGroup. Let's wait for half a second using a Delay command. That takes a single value, the amount of time to delay in seconds.

important

Delays should (almost) always be inside of SequentialGroups. A delay used inside a ParallelGroup will usually accomplish nothing.

An exception to this is if you want a ParallelGroup to take a minimum amount of time, then you can put a delay in it as well.

Let's create our delay:

val firstRoutine: Command
    get() = SequentialGroup(
        Lift.toHigh,
        ParallelGroup(
            Lift.toMiddle,
            Claw.close
        ),
        Delay(0.5)
    )

Finally, we can add our last ParallelGroup to our routine. The final routine looks like this:

val firstRoutine: Command
    get() = SequentialGroup(
        Lift.toHigh,
        ParallelGroup(
            Lift.toMiddle,
            Claw.close
        ),
        Delay(0.5),
        ParallelGroup(
            Claw.open,
            Lift.toLow
        )
    )

Step 3: Running our routine

Now that we have our routine, we just need to run it. To do this, let's override the onStartButtonPressed() function. To schedule a command, you can either call CommandManager.addCommand(commandToAdd), or you can just do commandToAdd(). In this case, let's do the latter.

override fun onStartButtonPressed() {
    firstRoutine()
}

Final result

That's it! You have created your very first autonomous program and, perhaps more importantly, learned about some of the tools you have at your disposal to create more complex autonomous programs.

Here is the final result:

@Autonomous(name = "NextFTC Autonomous Program Kotlin")
class AutonomousProgram: NextFTCOpMode(Claw, Lift) {
    val firstRoutine: Command
        get() = SequentialGroup(
            Lift.toHigh,
            ParallelGroup(
                Lift.toMiddle,
                Claw.close
            ),
            Delay(0.5),
            ParallelGroup(
                Claw.open,
                Lift.toLow
            )
        )

    override fun onStartButtonPressed() {
        firstRoutine()
    }
}

TODO

Commands

Why use commands? Commands allow you to organize your code much more efficiently than you could otherwise. They are an excellent alternative to finite state machines, but are a lot easier to create and modify, and can reach higher levels of complexity than a state machine can.

Parts of a Command

A command has four components: isDone, start, update, and stop.

  • isDone is checked every loop. If it ever evaluates to true, the command will stop running.
  • start is run once, when the command is scheduled. It is used for setting up starting states and doing other things that should only happen once.
  • update runs every loop, many times per second. Because of this, it is crucial that it never takes more than a trivial amount of time to execute. You should be extremely careful of looping or doing anything else that could take significant amounts of time
  • stop runs once when the command ends, and recieves a parameter of whether or not it was interrupted by a different command.
  • interruptible determines whether or not the command is able to be interrupted. A command is interrupted when another command is scheduled that requires a subsystem the command is using. If a command is not interruptable, then the new command will not run.
  • subsystems is a set of all the subsystems a command uses. This is used for determing when two commands requrie the same subsystem. This is passed to the constructor of most premade commands.

TODO

TODO