Robot Software
FRC 2

Prerequisites: RT 1
Corequisites: CS 1, CT 1

Disclaimer

While the goal every year is to create the most competitive robot possible with the tools and abstractions we've built up so far, we want everyone to understand the entire robot's code base each year. Therefore, reusing complicated code from previous year's robots is off-limits unless everyone understands how it works internally (if it breaks, they'll know how to attempt fixing it). Writing documentation or tutorials/labs is encouraged to help other students understand pieces of software quicker in the future.

Installation

Eclipse should already be installed from following the bootstrap page. For robot software development, follow the instructions here to install the FRC plugins and roboRIO ARM toolchain. Other robot software resources can be found here.

Design Patterns

Design patterns are used all over software development. Expert programmers know the idiomatic design patterns for their language to solve a problem quickly, efficiently, and with few bugs. We have common design patterns in robot software. A list of design patterns with descriptions, usage advice, and examples is provided below.

See this WPILib ScreenSteps page for more resources on performing various common robot tasks in C++. We have robot software from previous years on GitHub that students can study, understand, and potentially reuse.

General control flow

Under the covers

While we won't be using SampleRobot for writing production robot code, it is helpful to start with it to understand what our control flow abstractions (namely TimedRobot) are doing underneath.

The SampleRobot robot base class provides several virtual functions the subclass can override (see SampleRobot.h).

The functions Disabled(), Autonomous(), OperatorControl(), and Test() correspond to the modes in which the Driver Station can set the robot. They are called once upon switching to each mode. After one of the functions exits, RobotBase will continuously wait for Driver Station data until another mode change occurs. Therefore, code in each function should be run in a while loop until the mode changes. The following is an example Robot.cpp.

#include "Robot.hpp"

using namespace std::chrono_literals;

void Robot::Disabled() {
    while (IsDisabled()) {
        // Wait for robot to become enabled

        std::this_thread::sleep_for(20ms);
    }
}

void Robot::Autonomous() {
    while (IsAutonomous() && IsEnabled()) {
        // Perform autonomous tasks

        std::this_thread::sleep_for(20ms);
    }
}

void Robot::OperatorControl() {
    while (IsOperatorControl() && IsEnabled()) {
        // Perform normal robot actions and check for user input

        std::this_thread::sleep_for(20ms);
    }
}

void Robot::Test() {
    while (IsTest() && IsEnabled()) {
        // Perform robot test actions

        std::this_thread::sleep_for(20ms);
    }
}

TimedRobot

TimedRobot abstracts away the loops required for SampleRobot and provides better timing guarantees. TimedRobot provides Init() functions, which are run upon the Driver Station's transition into the corresponding mode, and Periodic() functions, which are run on an interrupt at a regular interval (~20ms) during the corresponding mode. They include:

RobotInit() and RobotPeriodic() are run in all modes.

TimedRobot's autonomous mode, for example, is roughly analogous to the following in SampleRobot.

using namespace std::chrono_literals;

void Robot::Autonomous() {
    AutonomousInit();

    while (IsAutonomous() && IsEnabled()) {
        AutonomousPeriodic();

        std::this_thread::sleep_for(20ms);
    }
}

Drivetrain with joysticks

There are multiple ways to drive a robot around; some are in open loop (the driver provides direct input to the system) and others are in closed loop (autonomous driving using encoders). This section discusses the former while the latter is covered in detail in CT 2.

The two most common ways to drive a robot manually are with WPILib's DifferentialDrive or MecanumDrive classes and with another drive class implementing a custom drive scheme. To make a four-wheeled robot drive around as quick as possible, the following is all that is required (although please don't put function implementations in the header file).

#include <CANTalon.h>
#include <Drive/DifferentialDrive.h>
#include <Joystick.h>
#include <TimedRobot.h>

class Robot : public frc::TimedRobot {
public:
    void TeleopPeriodic() {
        m_drive.CurvatureDrive(m_forwardJoy.GetY(), m_turnJoy.GetX());
    }

private:
    CANTalon m_flMotor{0};
    CANTalon m_rlMotor{1};
    frc::SpeedControllerGroup m_leftMotors{m_flMotor, m_rlMotor};

    CANTalon m_frMotor{2};
    CANTalon m_rrMotor{3};
    frc::SpeedControllerGroup m_rightMotors{m_frMotor, m_rrMotor};

    frc::DifferentialDrive m_drive{m_leftMotors, m_rightMotors};

    frc::Joystick m_forwardJoy{0};
    frc::Joystick m_turnJoy{1};
};

If more complex functionality and features is desired or your drive train is not supported by WPILIb, a custom drive scheme can be used instead. To implement one, the following is required.

See DifferentialDrive.cpp for examples.

Button edge detection

In TeleopPeriodic(), performing actions when the user presses a button is typically required. The simplest way to do this uses a Joystick and two booleans for tracking the past and current state of one of its buttons.

#include <Joystick.h>
#include <TimedRobot.h>

class Robot : public frc::TimedRobot {
public:
    void TeleopPeriodic() {
        // If button was pressed
        if (!oldButtonState && newButtonState) {
            std::cout << "Joystick button 2 pressed" << std::endl;
        }

        // If button was released
        if (oldButtonState && !newButtonState) {
            std::cout << "Joystick button 2 released" << std::endl;
        }

        // If button was held
        if (oldButtonState && newButtonState) {
            std::cout << "Joystick button 2 held" << std::endl;
        }

        // Update step
        oldButtonState = newButtonState;
        newButtonState = joystick.GetRawButton(2);
    }

private:
    frc::Joystick joystick{0};
    bool oldButtonState = false;
    bool newButtonState = false;
};

First, the booleans are initialized to false to represent a released button. At the end of every while loop iteration, the current state is copied to the old state and a new one is obtained from the joystick. By performing this operation once every loop iteration, we are guaranteed to see every rising or trailing edge and can act on it.

Unfortunately, as more buttons are checked, this approach becomes messy and unmaintainable. Also, the possibility of mistyping a boolean check increases and encourages copy-pasting code. WPILib provides functions in the Joystick class for this purpose.

#include <Joystick.h>
#include <TimedRobot.h>

class Robot : public frc::TimedRobot {
public:
    void TeleopPeriodic() {
        if (joystick.GetRawButtonPressed(2)) {
            std::cout << "Joystick button 2 pressed" << std::endl;
        }

        if (joystick.GetRawButtonReleased(2)) {
            std::cout << "Joystick button 2 released" << std::endl;
        }

        if (joystick.GetRawButton(2)) {
            std::cout << "Joystick button 2 held" << std::endl;
        }
    }

private:
    frc::Joystick joystick{0};
};

Toggle solenoid state

Toggling solenoids to extend or retract mechanisms can be done with the following pattern.

frc::Solenoid solenoid(1);

solenoid.Set(!solenoid.Get());

The following is a more verbose way of doing the same thing.

frc::Solenoid solenoid(1);

if (solenoid.Get()) {
    solenoid.Set(false);
} else {
    solenoid.Set(true);
}

Spin motor with joystick

This pattern is typically used to manually move mechanisms driven by a motor up or down.

frc::Joystick joystick(0);
CANTalon motor(1);

motor.Set(-joystick.GetY());

There is a negative sign because GetY() returns negative values when the joystick is pushed forward.

State machines

See the state machine and event framework labs for how we implement state machines, since they are commonly used tools.

Actuate motor to several points quickly

This is done when a mechanism driven by a motor should be moved from one position to another reliably and quickly. Note that this should only be done when the device could potentially be set to more than two positions within its range. If only two are required, a solenoid should be used instead. See the labs in CT 1 for examples on how to do this.

WPILib's encoder example may be helpful.

Project Setup

Each year, a new robot project will need to be created in Gerrit (our code review platform) and on GitHub for our competition robot code. First, an admin should create a new empty repository in the Team3512 GitHub organization (no README or license) and in Gerrit. Give them a name and description following the form of the previous years' repositories like "Robot-2017" and "The source code for the 2017 FRC robot.". Next, clone the repository from Gerrit and add the following files to it. See below the list for exceptions in downloading the provided files.

Generic Download

These files should be included in all of FRC Team 3512's software projects.

.gitreview will need the project name updated. COPYING will need the initial copyright year updated to the current year. README.md should be edited to be unique to the project.

Robot Download

These files should only be included in robot code projects. Some replace the generic files listed above.

README.md will need the year and robot name updated. "?" can be used until the robot name is chosen. src/Constants.hpp is a file we typically use every year and is provided as a placeholder so the src folder is added to Git history.

Generated via Robot Project

Change the following Eclipse project properties before copying the metadata files into the Git repository.

The optimization level can be restored to "None" with "Other optimization flags" of "-Og" and debug level can be restored to "Maximum" when needed for debugging. However, the settings described here are used for performance and should probably be used in competition.

Initial Commit

After the files are added to the repository, they should be committed in the first commit with the commit message "Initial Commit". This can then be pushed to Gerrit for review or pushed directly to master by admins. Gerrit will automatically mirror commits to master on GitHub.