Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Welcome to the Robot in Rust project for FRC! This is a safe, all-in-one solution for writing robot code in Rust. If you are tired of using Java or C++, maybe this project is for you.

This exact thing, of course, has been tried many times before. See FIRST Rust Competition, frcrs, and a few others you might find online. This project is different, because it’s more than just bindings to WPILib. We have a custom ECS-based scheduler (instead of Commands & Subsystems), logging system, autonomous system, simulation system, as well as the traditional WPILib, CTRE, REV, and (subset of) NetworkTables bindings.

Features

Normally, different functionality is separated into different modules created by different teams (Core functionality, CTRE motors, REV motors, Logging, Autonomoous, etc). In Rust, we don’t get this luxury because there are no other teams to write modules! So, the goal of this library is to support all those needs:

  • HAL bindings: A State type that sets everything up and runs your robot for you.
  • CTRE bindings: TalonFX, CANCoder, Pigeon2, a subset of configs.
  • REV bindings: SparkMax, Spark Relative & Absolute Encoders, a subset of configs.
  • Logging system: Similar to the AdvantageKit system, supports logging basic Rust types, Units, and automatically making your own types loggable via the AutoLog derive-macro. NetworkTables is supported.
  • Units: Everything, when possible, is unit-ed using uom. Helper functions are provided to make using these easier.
  • Math: Feedback and feedforward controllers, simple math types, kinematics, odometry, ported manually from WPILib. If a math type exists, it should work the same as it does in WPILib.
  • Autonomous System: Support for tank drive autonomous point-to-point path following, and (planned) support for swerve drive path following.
  • Simulation: Many of the WPILib simulation types are manually ported, and future support for integrating MuJoCo is planned.

Commands and Subsystems

The main difference between this and WPILib is the absence of the Commands and Subsystems framework. Instead we use the ECS from the Bevy Game Engine, which is lightweight and much more suited for Rust than Commands.

Who this is for

To use this project for serious robot code (meaning you are bringing it to a competition or there will be time-pressure to fix bugs should they occur) you should be experienced in Rust and low-level programming, and have a backup robot program written in Java or C++.

If you’re using this for other less serious projects, regardless of your experience you should give this a try and see if you can figure it out.

Regardless, you should still read through this book, and use it as a reference in the future. Good luck!

Getting Started

All you need installed is WPILib. If it’s already installed it should appear in your home directory. You can follow the official guide here. This library officially supports Linux and MacOS, and Windows support is planned.

You will also need to add the WPILib compiler toolchain to your PATH. On Mac and Linux, you have to add some code to the .zshrc or .bashrc file in your home directory. Open it in a text editor and add this line to the end:

export PATH="[home directory here]/wpilib/2026/roborio/bin:$PATH"

Warning

The home directory cannot be ~, it must be /Users/username on Mac or /home/username on Linux.

Thirdly, you will need to install the Rust target for the roboRIO. To do this, you will need rustup installed. Then, run:

rustup target add arm-unknown-linux-gnueabi

Project setup

Let’s create a new project. You can call it whatever you want, but I’m going to call it robot. In the directory you want your project directory to go in, run:

$ cargo new robot

Next, cd into the project directory and add the robot-rust crate (you don’t have to rename it, but it will save you some typing in the future):

$ cd robot
$ cargo add robot-rust --rename frc

Now, to make sure cross-compiling to the roboRIO target goes smoothly, copy the .cargo/config.toml from the GitHub repository into your project.

You can now open up the src/main.rs file that Cargo generated. You should see the typical “Hello, World” main function. This would be enough if we were running it on our own computer, but we need more code to run it on the robot! First, import the robot-rust prelude:

use frc::prelude::*;

Virtually every type and function you will ever need to use gets imported with the prelude. CTRE and REV each have their own preludes, though.

Now, we need to create a new State. In the main function, you will configure this state and then run it. The state is what holds all the resources and systems that you create for your project.

fn main() {
    let mut state: State = State::new().unwrap();
}

Now, we have to create a system that will run when the robot program starts. Later we’ll learn exactly what a system is, but for now just think of it as a function. Outside of main, add:

fn hello_robot() {
    println!("Hello, Robot!");
}

We then give this to state, which will handle running it at the right time, and run the program:

fn main() {
    // ...
    state.add_systems(Init, hello_robot);
    state.run();
}

Viewing the full code

Each chapter where you learn to write code has a corresponding file in the examples directory on GitHub. For example, this chapter’s example is called hello_robot.rs.

Note

In example code the library is not renamed, so it is still robot_rust instead of frc.

Deploying code

Provided is a deploy.sh script that will automatically build and deploy the program to your robot. You can find it in the GitHub repository. Download it and put it in your project directory, and run the following command to make it executable:

$ chmod +x deploy.sh

Use your perferred text editor to open deploy.sh

You will need to edit the variables set in the program:

  • Update PROJECT_NAME to be the name of your project (in this case we called it robot).
  • Update TEAM to be your team number
  • Update ROBOT_IP to be the IP address of the roboRIO when it is connected to your computer. If the script fails, it is most likely that this is wrong or the robot isn’t connected properly. You can run ping IP_HERE to test if the address is working.

Once these variables are set properly, save deploy.sh. You can run the deploy script, which will handle building, deploying, and running the robot program on the RIO.

$ ./deploy.sh

If this works, the easiest way to see that the code ran is by looking in the DriverStation. Otherwise, if you know how you can look at the log file on the RIO.

Viewing documentation

This documentation should be enough to get you understanding everything, but sometimes you need a more specific reference. For various reasons, the docs on docs.rs can’t build, so you need to generate them locally like so:

$ cargo doc --package robot-rust --open

This will open the documentation in your browser.

Systems, Resources, and Schedules

This project manages everything using an ECS, which stands for Entity Component System. We will learn more later about the purpose of Entities and Components, but for now we only need to know how Systems work.

A system is anything that takes some inputs, and modifies some of those inputs. You saw an example of a system called hello_world in the previous chapter, although this one took no inputs. In reality, systems can (and usually do) take many inputs and many types of inputs. The most common input you will be using is called a Resource.

Resources

Any type can be a resource. There can only be one resource per type (put simply, there can’t be more than one Intake resource), and they must be instantiated when the program starts. If you’ve done some FRC programming before, you’ll probably notice that this design is very similar to what a Subsystem is. Subsystems are stored in resources, but also are controllers, program state, and many other things. For the most part, all the inputs to your systems will be resources.

Schedules

In the previous chapter, we wrote state.add_systems(Init, hello_robot). Init is a schedule, which specifies when a system should be ran. There are a lot of different schedules:

  • Init: Runs when the robot program starts
  • DisabledInit/TeleopInit/AutonomousInit/TestInit: Runs when a certain state of the robot program starts.
  • DisabledExit/TeleopExit/AutonomousExit/TestExit: Runs when a certain state of the robot program ends.
  • Periodic: Runs every ~20ms.
  • DisabledPeriodic/TeleopPeriodic/AutonomousPeriodic/TestPeriodic: Runs every ~20ms during a certain stage of the robot program.

The State

In the main function, the program creates the State. You can configure how the program will run using the state, which holds both the systems and the resources used by the systems. Then, after calling state.run(), the state will handle running systems at the correct time, and passing the resources into the systems.

We saw a basic example of the usage of the State in the previous chapter:

fn main() {
    let mut state: State = State::new().unwrap(); // Create the state.
    state.add_systems(Init, hello_robot); // Tell the state to run hello_robot when the program initializes
    state.run(); // Now the program runs, and state will handle calling hello_robot at the correct time.
}

When calling run, the state first runs the Init schedule. Then, it enters into a loop that runs until the program is terminated:

  • First it polls all the data from the driver station.
  • Then, if the mode changes (for example from Disabled to Teleop), it runs the matching schedules (DisabledExit, TeleopInit).
  • Then, it runs the periodic schedule for the respective mode (like TeleopPeriodic).
  • Lastly, it runs the general Periodic schedule.

During each schedule, the system you assigned to it will run. The resources you specified (if available) will be passed into the system, which the system can then modify. Resources are global, so if you modify them in one system or in one schedule, it will be modified for everything else.

Now that we’ve got all the theory out of the way, we can see how to actually do things.

Creating a resource

For now, let’s just create a resource that wraps a single motor. We’ll derive Deref and DerefMut so that we can interact with the type as if it was just a motor (you can read about what those do here).

For this example we’ll use CTRE’s TalonFX motor controller. If you want to use REV, you can simply replace wherever it says TalonFX with SparkMax.

We’ll start by creating a MyMotor resource:

#[derive(Deref, DerefMut)]
struct MyMotor(TalonFX);

This resource can be added to state like so (you can set the CAN ID and CAN Bus to be whatever corresponds to your actual motor):

state.insert_resource(MyMotor(TalonFX::new(1, "*")));

The resource we’ve added will now be available for use in systems. We can now write a system that will use it.

Using a resource

The resource can now be used in a system. All resource arguments to a system need to be wrapped in either the Resource or ResourceMut type. For example, we’ll create a system called spin that will spin MyMotor:

fn spin(mut motor: ResourceMut<MyMotor>) {
    motor.set_voltage(5.0.volts());
}

The volts function is provided by the library so you can easily create unit-ed types. We use it because TalonFX::set_voltage requires a Voltage (or ElectricPotential) type.

For our purposes, we will use the TeleopPeriodic schedule to spin our motor. So after registering the resource, we can add the system like so:

state.add_systems(TeleopPeriodic, spin);
state.run();

When you call state.run() and go into Teleop mode, the state will see that the spin system takes the MyMotor resource. Knowing that, it will automatically route the resource into the system, which can then modify the resource.

Now, you should have a spinning motor!

Warning

Systems in a schedule don’t run in any specified order, and will usually run in different orders every iteration. If you want to specify the order systems are ran, you can use state.add_systems(Schedule, (first, second).chain());.

Game state

There are some resources that are automatically added which can give you information about the game state.

  • There is the Mode enum, which specifies whether the robot is disabled, in teleop, or autonomous (although using schedules is usually preferred).
  • You can also get the Alliance (Red, Blue, or Unknown), and the AllianceStation (Red1, Red2, Blue1, etc).
  • Lastly there is the Model enum, which is either Simulation or Competition.

All these are added when the state is created, before the Init schedule (besides Mode, which will be None, but you can assume the robot will be disabled).

Commands

This library does feature a commands system. However, although it has the same name, it is very different from the commands you may have used in Java or C++. Commands are a type of resource, and thus each command is a unique type. For example, the type for a motor spinner command would look like this:

struct SpinMotorCommand;

Notice that the command has no member values. That is because you cannot set the speed of a SpinMotorCommand. That is specified by the implementation. We could also let the command dispatcher set the voltage like so:

struct SpinMotorCommand {
    voltage: Voltage
}

However, usually it’s best practice to abstract these values into the implementation to ensure the behavior of the command is consistent across the entire program.

We can then register the command using state:

state.register_command(SpinMotorCommand);

Note that registering a command doesn’t make it run. Commands are disabled by default. If we wanted the command to be enabled immediately, we could use .enabled() on the output of register_command:

state.register_command(SpinMotorCommand).enabled();

If you don’t do this, you can enable any command in a system like so:

fn enables_command(mut command: CommandMut<SpinMotorCommand>) {
    command.enable();
}

Attaching this system to the Init schedule would function the same as calling .enabled() above.

Handlers

Just as a command is itself a resource, the code that handles running the command is also a system. The system takes the command wrapped in the Command helper type, along with whatever other data it needs. The header for SpinMotorCommand would look like so:

fn spin(command: Command<SpinMotorCommand>, mut spinner: ResourceMut<Spinner>) { ... }

Usually, we don’t want to mutate the command itself, but if we did we could replace the first parameter with mut command: CommandMut<SpinMotorCommand>.

When the command is enabled, the handler should set the voltage of the spinner to some positive value, and when the command ends we should set the voltage of the spinner to zero:

fn spin(spin: Command<SpinMotorCommand>, mut spinner: ResourceMut<Spinner>) {
    if spin.enabled() {
        spinner.set_voltage(5.0.volts());
    }

    if spin.just_ended() {
        spinner.set_voltage(0.0.volts());
    }
}

Usually it’s a better idea to use just_ended() instead of disabled(), just in case some other command or system wants to set the voltage of the spinner in the future.

This system can be registered like any normal system:

state.add_systems(TeleopPeriodic, spin);

But usually it’s easier to read when we group the command handler along with the code that registers the command itself:

state
    .register_command(SpinMotorCommand)
    .with_handler(TeleopPeriodic, spin);

Triggers

A trigger is something that tells a command when to run. It is similar to a system in that it can take any arguments, but it must return a bool. Let’s say we want to run the SpinMotorCommand command only when the user presses the “A” button on the controller:

fn spin_trigger(controller: Resource<XboxController>) -> bool {
    controller.a()
}

Note

The XboxController needs to be inserted as a resource.

We can then tell the command to only run on the trigger:

state
    .register_command(SpinMotorCommand)
    .with_handler(TeleopPeriodic, spin)
    .while_trigger(spin_trigger);

The while_trigger function will constantly set the command’s enabled state based on the value of the trigger. If this is used, it will have only bad affects to enable or disable the command anywhere else in your code. If you wish to only enable the command when a trigger changes from false to true, and not disable it, use on_trigger. If you wish to enable and disable the command only when the trigger’s state changes, use with_trigger.

With that, you have everything you need to use commands!

Note

Creating triggers for each joystick button can be tedious. Assuming the XboxController is registered as a resource, the triggers you need already exist:

state
    .register_command(SpinMotorCommand)
    .with_handler(TeleopPeriodic, spin)
    .on_trigger(XboxController::a_trigger);

If you have a custom controller resource, these won’t work and you’ll have to create your own functions.

Subsystem Design

In Java and C++, the choice of how to encapsulate different systems on the robot was largely made up for you: Implement the Subsystem interface, and register commands on that subsystem. In Rust, that decision is completely yours, and truly it is. If you have a design you think would work better for your robot or for your team, by all means skip this chapter. Here, I will layout a design you can use that is both similar to the old framework, but still elegant and functional within the new.

With that said, the first type that almost all subsystem designs have is the one that contains the hardware interface, like motors and encoders. This type will be private, so no other subsystems directly interface with it. For this section, we’ll use an intake subsystem as an example:

// In intake.rs, seperate file from main.rs

struct Intake {
    motor: TalonFX,
    pivot_motor: TalonFX,
    encoder: CANcoder,
}

impl Intake {
    fn new() -> Self {
        // Code to do this not specified ...
    }
}

We will then define another type, IntakeTargets, that will be public. The intake subsystem will be responsible for getting the physical hardware to these targets. Any other code will be able to modify these targets, so you should make sure multiple peices of code aren’t modifying it at the same time. Checks in the Commands system are planned to make sure this doesn’t happen, but do not yet exist.

For the intake, our targets specify the pivot position and the intake state.

pub enum IntakeState {
    Stopped,
    Intake,
    Outtake,
}

pub enum PivotState {
    Stowed,
    Extended,
}

pub struct IntakeTargets {
    pub intake: IntakeState,
    pub pivot: PivotState,
}

impl IntakeTargets {
    pub fn new() -> Self {
        Self {
            intake: IntakeState::Stopped,
            pivot: PivotState::Stowed,
        }
    }
}

We’ll then create some function that reads the targets and modifies the hardware:

#![allow(unused)]
fn main() {
fn periodic(mut intake: ResourceMut<Intake>, targets: Resource<IntakeTargets>) {
    // Code to do this not specified ...
}
}

We are already almost finished making our Subsystem. We just need a function that can be called from main.rs that inserts the Intake and IntakeTargets resources and adds the periodic system:

#![allow(unused)]
fn main() {
pub fn setup(state: &mut State) {
    state.insert_resource(Intake::new());
    state.insert_resource(IntakeTargets::new());

    // Make sure to add the system in both teleop and autonomous.
    state.add_systems(TeleopPeriodic, periodic);
    state.add_systems(AutonomousPeriodic, periodic);
}
}

Now we have a usable Subsystem. We can call the setup from the main function:

// main.rs

mod intake;

fn main() {
    // ...
    intake::setup(&mut state);
    state.run();
}

Subsystem commands

Up until this point, we’ve ignored the use of commands with Subsystems. For this, depending on the complexity of the subsystem, you have two options. If the subsystem is relatively simple, like the intake we wrote earlier, you can replace the targets with a few commands (Intake, Outtake, Stow, and Extend, for example), and have the command handlers directly interface with the hardware. For more complex subsystems (like a drivetrain), it will probably make more sense for commands to interface with a targets type, which would be in turn handled by something like the periodic function we wrote earlier.

Logging

A logger is already setup for your use. It logs to the wpilog file on the roboRIO, as well as to NetworkTables. Tools such as AdvantageScope can read these logged values.

Most basic types implement the Loggable trait (booleans, floats, integers, &str, String, and arrays of all those values). You can log them in a system using the Logger resource like so:

#![allow(unused)]
fn main() {
fn log(mut logger: ResourceMut<Logger>) {
    logger.append_value("Path/Number", 5.0);
}
}

The append_value function is only for basic values that implement Loggable. If you want to log types that have multiple values, you should use the append function (which supports all Loggable types as well).

AutoLog

Similar to AdvantageKit, there is an AutoLog trait which you can automatically implement for any struct that is made up of types that themselves implement AutoLog.

For example, the state for an intake subsystem could be specified like so:

#![allow(unused)]
fn main() {
#[derive(AutoLog)]
struct IntakeState {
    speed: AngularVelocity,
    angle: Angle,
}
}

You could then log this by calling logger.append("Intake", intake_state). Note that unit types also can all be logged.

You can also nest loggable types:

#![allow(unused)]
fn main() {
#[derive(AutoLog)]
struct IntakePivotState {
    angle: Angle,
    velocity: AngularVelocity,
}

#[derive(AutoLog)]
struct IntakeState {
    velocity: AngularVelocity,
    pivot: IntakePivotState,
}
}

The logging tree for the new IntakeState would look like this:

  • Intake/velocity
  • Intake/pivot/angle
  • Intake/pivot/velocity

Simulation

Advanced

This project is not stable, and so before long you may find yourself looking for a feature or binding that does not yet exist, or seeing foreign-looking errors such as segmentation faults. The advanced section is designed to help you solve these problems yourself, instead of needing to wait for a maintainer to fix them.

Development Environment

Most of these topics will involve modifying the source code, so if you do end up having to do that either to add a missing feature or to fix a bug, please contribute it via a pull request.

The first step of setting up a dev environment is to clone

Segmentation Faults

Segfaults are a failure condition that occurs when the program tries to read memory that it doesn’t have access to. Most of the time, this means a pointer that should have pointed to data actually pointed to NULL. A segfault is similar to the NullPointerException in Java.

One frustrating thing about segfaults in compiled programs is that, by default, when they occur no debug information is printed. Only the text Segmentation fault (core dumped) prints, and the program exits. After this occured once, I installed a simple signal handler into the library so that instead this is the text you will see:

====================================
FATAL CRASH: SIGSEGV (Segmentation Fault); Signal 11.
Sorry about this! This probably indicates a bug with the C/C++ bindings.
This bug should be reported.
If you're at a competition, try to run the program once more, and if it fails, switch to a backup.
====================================

<rust stack trace here>

Causes

In the simplest case, it may be possible that an explicitly null reference is being passed to the function. That should be pretty clear just by looking at the code.

In other cases, it is likely a lifetime issue, which will probably be much harder to debug. When the rust compiler believes a value will no longer be used, it generates code to drop it. If data still exists that referenced that dropped data, a segfault will occur when it is used.

The safe wrappers do their best to prevent this from happening, for example by tying the lifetime of a configurator to its motor. Unfortunately, they aren’t always perfect.

One binding that is specifically of note is the NetworkTables binding. The NetworkTablesTopic type should have its lifetime bound to the NetworkTablesInstance type, and the NetworkTablesPublisher to its topic. To prevent complexity with internal references (fields that reference other data in the same struct) in the Logger system, and because these are generally not used by the end-user, these lifetimes don’t exist. If you are using these manually, make sure that the instance is not dropped before the topic.

Another possible culprit is the CTRE or REV bindings to devices and their configurators. The configurator bindings are vast and deal with many types of data, often with two layers of bindings. If you have the stack trace, it should be fairly easy to tell if this is the problem.

Hopefully one of these helped. I will be adding more if I encounter more problems as the library goes through more testing.

Contributing & Writing Bindings

Many features are not yet complete. For some of those, such as the autonomous or simulation systems, it’s not feasible to write documentation on how to expand them. Missing bindings to vendor libraries, however, are fairly trivial to write. There are two types of bindings: direct binding, where we call C++ code, and porting, where we entirely rewrite the code. Direct binding is generally used for hardware or some more complex types, while porting is used for types that are simple, or where we can’t sacrifice the performance cost of bindings.

For either of these methods, you will need experience in C/C++ and unsafe Rust.

Direct Bindings

Once you know how to do it, writing direct bindings is definetely the easier, but more tedious, of the two methods.