Training Introduction

This website contains the Software training for Team 2537. Everything you need to know is linked here.

Training Schedule

We have about 12 weeks of training. Each week includes two meetings and about 4 hours of meeting time. Specific days and times will be announced by the team captain (Software doesn't choose these times).

Software Initialization - 1 week

Software Expectations Presentation: Software on FRC 2537
Setting up your development environment (git and Github too!): Setting up your development environment

Kotlin Programming - 3 weeks

Learning Kotlin

We will go through all of the Kotlin lessons, grouped into 2 units. Each unit will have a project at the end.

Robot Programming - 4 weeks

Robot Programming

You will learn to program a robot and will start to deploy code to a robot and drive it around (this is fun).

Other Stuff - 1 week

Introduction to our team's development practices: Coding Practices

Challenges - 3 weeks

You will recieve challenges with a group and will have to complete them before the end of the week (this is really fun).

Kotlin

Kotlin is a modern programming language that is designed to be fully interoperable with Java. It is a statically-typed language that runs on the Java Virtual Machine (JVM). Team 2537 uses Kotlin for programming the robot.

If you already know Java, you will find Kotlin to be very similar. However, Kotlin has many features that make it more concise and expressive than Java. It also has some features that are not present in Java, such as extension functions and data classes.

In this training, you will receive an introduction to Kotlin and learn about the basics of the language. You will learn about variables, operators, logic statements, loops, functions, classes, objects, and more. You will also complete projects to test your knowledge.

Our goal is to provide you with the resources you need to learn. You will probably need to use these resources on your own time to fully understand the concepts. If you have any questions, feel free to ask a lead or mentor for help.

These lessons are designed to be completed in order. Each lesson builds on the previous one, so it is important to complete them in order. If you miss a meeting or fall behind, you can always catch up by reading the lessons and reading through the linked materials. We will not wait for you during meeting time.

At the end of every lesson, there will be a short exercise for you to complete. The code template for the exercise will be provided, and you will need to fill in the missing parts.

How to do our Kotlin Labs

Each module of Unit 1 has a matching lab that tests your knowledge of what you just learned. To do each lab you will want to clone this repository, and make a branch titled firstname_lastname, replacing with your actual first and last name.

Once you have your own branch, you'll want to open the project in IntelliJ, and navigate to src/main/kotlin/basics/unit1, and there you will see all five labs. Once you have written the code you can hover over the main() function, and hit the little green play button to the left of it, this will run your code so you can see if it works properly or not.

Once you finish with each lab, commit to your branch, and ask a software lead or mentor to take a look, and thats it!

Unit 1

In this Unit, you will learn the basics of Kotlin programming. You will learn about variables, operators, logic statements, loops, and functions. You will also complete a project to test your knowledge.

Unit 1:

  • Variables & Operators
  • Logic and Loops
  • Functions
  • Classes & Properties
  • Methods & Constructors

Variables & Operators

In this lesson, you will learn about variables and operators in Kotlin. You will learn how to declare variables, assign values to variables, and use operators to perform operations on variables. We will also quickly cover comments.

Comments

Just to make sure everyone is fully able to read the examples below, a "comment" is text in a code file that gets ignored. In kotlin single line comments are made with // and everything on the line after this is ignored. Multi-Line comments start with /* and end with */, all text between those are ignored.

// This is a single line comment!

/*
This
Is
A
Multi-Line
Comment
 */

What is a Variable?

A variable is a storage location that holds a value. In Kotlin, you can declare a variable using the var keyword. The syntax for declaring a variable is as follows:

var variableName: DataType = value
  • var: keyword used to declare a variable
  • variableName: name of the variable
  • DataType: type of the variable
  • =: assignment operator
  • value: value assigned to the variable

When you use the var keyword to declare a variable, you can change the value of the variable later in the program.

Example

var number: Int = 10
println(number) // Output: 10
number = 20 // Will not cause an error
println(number) // Output: 20

In the example above, we declare a variable number of type Int and assign it a value of 10. We then print the value of number, which is 10. Next, we assign a new value of 20 to number, and print the value of number, which is now 20.

If you want to declare a variable that cannot be changed later in the program, you can use the val keyword. The syntax for declaring a constant variable is as follows:

val variableName: DataType = value

All other aspects of declaring a constant variable are the same as declaring a variable with the var keyword.

Example

val pi: Double = 3.14159
var radius: Double = 5.0

println(pi * radius * radius) // Output: The area of a circle with radius 5.0

radius = 10.0 // Will not cause an error
pi = 3.0 // Will cause an error

Data Types

In Kotlin, variables have data types that specify the type of data they can hold. The following table lists some of the common data types supported by Kotlin:

Data TypeDescription
IntWhole number, no decimal places
DoubleNumber with ~15 decimal places
FloatNumber with ~7 decimal places
Booleantrue or false
CharSingle character
StringSequence of characters

Example

var age: Int = 16
var pi: Double = 3.14159
var isStudent: Boolean = true
var initial: Char = 'A'
var name: String = "John Doe"

var height: Int = "Five Feet" // Causes an error because the data type is incorrect
var weight = 150 // Kotlin can infer the data type, so you don't need to specify it

Type Inference

When a variable is given a value immediately, with its declaration, the type of the variable will automatically be chosen, or inferred. However, it is often recommended to specify the data type to avoid confusion.

Nullability

In Kotlin, variables can be nullable, meaning they can hold a null value. To declare a nullable variable, you can use the ? operator after the data type. Be careful when using nullable variables, as they can cause unexpected errors if not handled properly.

Example

var name: String? = null
println(name) // Output: null

Operators

Operators are symbols that are used to perform operations on variables and values. Kotlin supports a variety of operators, including arithmetic, assignment, comparison, and logical operators.

Arithmetic Operators

Arithmetic operators are used to perform mathematical operations on variables and values. The following table lists the arithmetic operators supported by Kotlin:

OperatorDescription
+Addition
-Subtraction
*Multiplication
/Division
%Modulus (remainder)

Example

var x: Int = 10
var y: Int = 5

println(x + y) // Output: 15

Assignment Operators

Assignment operators are used to assign values to variables. The following table lists the assignment operators supported by Kotlin:

OperatorDescription
=Assigns the value on the right to the variable on the left
+=Adds the value on the right to the variable on the left
-=Subtracts the value on the right from the variable on the left
*=Multiplies the variable on the left by the value on the right
/=Divides the variable on the left by the value on the right
%=Assigns the remainder of the division of the variable on the left by the value on the right to the variable on the left

Example

var x: Int = 10 // Assigns 10 to x
x += 5 // Adds 5 to x
println(x) // Output: 15

Comparison or Boolean Operators

Comparison or boolean operators are used to compare values and return a boolean (true/false) result. The following table lists the comparison operators supported by Kotlin:

OperatorDescription
==Equal to
!=Not equal to
>Greater than
<Less than
>=Greater than or equal to
<=Less than or equal to

Example

var x: Int = 10
var y: Int = 5

println(x > y) // Output: true
println(x == y) // Output: false

Logical Operators

Logical operators are used to combine multiple boolean expressions and return a boolean result. The following table lists the logical operators supported by Kotlin:

OperatorDescription
&&Returns true if both values are true
\|\|Returns true if at least one value is true
!Returns true if the value is false

Example

var x: Int = 10
var y: Int = 5

println(x > y && x < 20) // Output: true
println(x > y || x == 5) // Output: true

String Formatting

Although not an operator, string formatting is a useful technique for combining strings and variables. In Kotlin, you can use string interpolation to insert variables into strings. To use string interpolation, prefix the variable name with a dollar sign ($). You may also enclose the variable name with curly-braces (e.g. ${myVar}).

Example

var name: String = "Alice"
var age: Int = 30

println("Name: $name, Age: $age") // Output: Name: Alice, Age: 30

Logic & Loops

In this lesson, you will learn about "Control Flow" in Kotlin. You will learn how to use conditional statements and loops to control the flow of your program.

Conditional Statements

Conditional statements allow you to execute different blocks of code based on certain conditions. In Kotlin, you can use the following conditional statements:

  • if statement
  • if-else statement
  • when statement

if Statement

The if statement is used to execute a block of code if a condition is true. The syntax for the if statement is as follows:

if (condition) {
    // Code to be executed if the condition is true
}

If the condition is true, the code block inside the curly braces {} will be executed. If the condition is false, the code block will be skipped.

Example

val number = 10

if (number > 0) {
    println("Number is positive")
}

In the example above, we declare a variable number with a value of 10. We then use an if statement to check if number is greater than 0. Since number is 10, the condition is true, and the message "Number is positive" will be printed.

if-else Statement

The if-else statement is used to execute one block of code if a condition is true and another block of code if the condition is false. The syntax for the if-else statement is as follows:

if (condition) {
    // Code to be executed if the condition is true
} else {
    // Code to be executed if the condition is false
}

If the condition is true, the code block inside the first set of curly braces {} will be executed. If the condition is false, the code block inside the second set of curly braces {} will be executed.

Example

val number = -5

if (number > 0) {
    println("Number is positive")
} else {
    println("Number is negative")
}

In the example above, we declare a variable number with a value of -5. We then use an if-else statement to check if number is greater than 0. Since number is -5, the condition is false, and the message "Number is negative" will be printed.

You can also chain multiple if-else statements together to check for multiple conditions.

Example

val number = 0

if (number > 0) {
    println("Number is positive")
} else if (number < 0) {
    println("Number is negative")
} else {
    println("Number is zero")
}

In the example above, we declare a variable number with a value of 0. We then use multiple if-else statements to check if number is greater than 0, less than 0, or equal to 0. Since number is 0, the message "Number is zero" will be printed.

when Statement

The when statement allows you to execute different blocks of code based on a more complex state than true/false. this is useful when you have multiple conditions to check, so you don't have to chain multiple if-else statements.

The syntax for the when statement is as follows:

when (variable) {
    value1 -> {
        // Code to be executed if variable equals value1
    }
    value2 -> {
        // Code to be executed if variable equals value2
    }
    else -> {
        // Code to be executed if variable does not equal any of the values
    }
}

The when statement checks the value of the variable and executes the code block corresponding to the value. If the variable does not match any of the values, the code block inside the else block will be executed.

Example


val number = 10

when (number) {
    0 -> println("Number is zero")
    in 1..9 -> println("Number is between 1 and 9")
    else -> println("Number is 10 or greater")
}

In the example above, we declare a variable number with a value of 10. We then use a when statement to check the value of number. If number is 0, the message "Number is zero" will be printed. If number is between 1 and 9, the message "Number is between 1 and 9" will be printed. If number is 10 or greater, the message "Number is 10 or greater" will be printed.

Loops

Loops allow you to execute a block of code multiple times. In Kotlin, you can use the following loop structures:

  • for loop
  • while loop
  • do-while loop

But first we need to quickly go over ranges and collections.

Ranges

You can create a range of numbers by specifying the start and end values separated by the .. operator. The range includes the start value and the end value.

val range = 1..5 // {1, 2, 3, 4, 5}

You can also create a range that counts down by using the downTo keyword.

val range = 5 downTo 1 // {5, 4, 3, 2, 1}

Collections

Collections are used to store multiple values. In Kotlin, there are many types of collections, but most of them are for you to learn about on your own. For this lesson we will just be talking about lists

A list is an ordered collection of elements. In Kotlin, you can create a list using the listOf function.

val numbers = listOf(1, 2, 3, 4, 5)

You can access elements in a list using the index of the element.

println(numbers[0]) // Output: 1

List indices start at 0, so the first element in the list has an index of 0.

for Loop

The for loop is used to iterate over a range, collection, or any other type of iterable. The syntax for the for loop is as follows:

for (element in range) {
    // Code to be executed for each element
}

The for loop will iterate over each element in the range and execute the code block for each element. the element variable will be assigned to the current element in the range.

Example

val numbers = listOf(1, 2, 3, 4, 5)

for (number in numbers) {
    println(number)
}

In the example above, we create a list of numbers 1, 2, 3, 4, 5 and use a for loop to iterate over each number and print it to the console.

You can use a shorthand for the for loop by using the .forEach function. When doing this you can use the it keyword to refer to the current element.

Example


val numbers = listOf(1, 2, 3, 4, 5)

numbers.forEach {
    println(it)
}

while Loop

The while loop is used to execute a block of code as long as a condition is true. The syntax for the while loop is as follows:

while (condition) {
    // Code to be executed while the condition is true
}

The while loop will continue to execute the code block as long as the condition is true.

Example

var number = 5

while (number > 0) {
    println(number)
    number--
}

In the example above, we declare a variable number with a value of 5. We then use a while loop to print the value of number and decrement it by 1 until number is less than or equal to 0.

You can also use the do-while loop, which is similar to the while loop but will always execute the code block at least once before checking the condition.

Example

var number = 5

do {
    println(number)
    number--
} while (number > 0)

In the example above, we declare a variable number with a value of 5. We then use a do-while loop to print the value of number and decrement it by 1 until number is less than or equal to 0.

Functions

Functions allow you to define a block of code that can be executed multiple times with different inputs. In Kotlin, you can define functions using the fun keyword. The syntax for defining a function is as follows:

fun functionName(parameter1: DataType, parameter2: DataType, ...): ReturnType {
    // Code to be executed
    return value
}
  • fun: keyword used to define a function
  • functionName: name of the function
  • Parameters (Same syntax as variables)
    • parameter1, parameter2, ...: parameters passed to the function
    • DataType: type of the parameters
  • ReturnType: type of the value returned by the function
  • return: keyword used to return a value from the function
  • value: value to be returned by the function

Example

fun add(a: Int, b: Int): Int {
    return a + b
}

val sum = add(5, 3)
println(sum) // Output: 8

In the example above, we define a function add that takes two parameters a and b of type Int and returns their sum. We then call the function with arguments 5 and 3 and store the result in a variable sum. Finally, we print the value of sum, which is 8.

Functions often return a value using the return keyword, but not all of them do. If a function does not return a value, it has a return type of Unit. Unit is similar to void in other languages and indicates that the function does not return anything. Such functions are often used to perform repetitive and common tasks, such as outputting text with println().

Example

fun greet(name: String) { // Return type is Unit, no need to specify
    println("Hello, $name!")
}

greet("Alice") // Output: Hello, Alice!

In the example above, we define a function greet that takes a parameter name of type String and prints a greeting message. The return type of the function is Unit, which is not explicitly specified. For the sake of conciseness, Unit is almost always omitted as a return type.

You can also define functions with default values for parameters. This allows you to call the function without providing values for all parameters. If you do not provide a value for a parameter with a default value, the default value will be used.

Example

fun greet(name: String = "World") {
    println("Hello, $name!")
}

greet() // Output: Hello, World!
greet("Alice") // Output: Hello, Alice!

In the example above, we define a function greet that takes a parameter name of type String with a default value of "World". We then call the function without providing a value for name, which uses the default value. We also call the function with the argument "Alice", which overrides the default value.

If you have multiple parameters, the ones with default values must come after the ones without default values. You can also use named arguments to specify the values for parameters with default values.

Example

fun greet(greeting: String = "Hello", name: String = "World") {
    println("$greeting, $name!")
}

greet() // Output: Hello, World!
greet("Hi") // Output: Hi, World!
greet("Hi", "Alice") // Output: Hi, Alice!
greet(name = "Bob") // Output: Hello, Bob!

In the example above, we define a function greet with two parameters greeting and name, both of type String, with default values of "Hello" and "World", respectively. We then call the function with different combinations of arguments and named arguments to see how the default values are used.

Functions can also be defined as expressions using the = operator. This allows you to define functions concisely without using curly braces and the return keyword.

Example

fun add(a: Int, b: Int) = a + b

val sum = add(5, 3)
println(sum) // Output: 8

In the example above, we define a function add that takes two parameters a and b of type Int and returns their sum. The function is defined as an expression using the = operator, which automatically returns the result of the expression.

The main Function

In Kotlin, the main function is the entry point of a Kotlin program. When you run a Kotlin program, the code inside the main function is executed. The main function is defined as follows:

fun main() {
    // Code to be executed
}

When you run a Kotlin program, the code inside the main function is executed sequentially. You can define other functions and call them from the main function to organize your code.

Lambdas

Lambda expressions (also known as anonymous functions) are a concise way to define functions that can be passed as arguments to other functions. In Kotlin, functions are first-class citizens, which means they can be treated like any other value. This allows you to pass functions as arguments, return functions from other functions, and store functions in variables.

Lambda Syntax

Lambda expressions are defined using curly braces {} and the -> operator. The syntax for a lambda expression is as follows:

{ parameter1: DataType, parameter2: DataType, ... -> 
    // Code to be executed
}
  • {}: curly braces used to define a lambda expression
  • parameter1, parameter2, ...: parameters passed to the lambda expression
  • DataType: type of the parameters
  • ->: operator used to separate parameters from the code block
  • // Code to be executed: code block to be executed by the lambda expression
  • The last expression in the lambda block is the return value

These lambda expressions can be assigned to variables and passed as arguments to other functions, which allows you to write more concise and expressive code.

Example

val add: (Int, Int) -> Int = { a, b -> a + b }
val multiply: (Int, Int) -> Int = { a, b -> a * b }

fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

In the example above, we define two lambda expressions add and multiply that take two parameters a and b of type Int and return their sum and product, respectively. We then define a function calculate that takes two parameters a and b of type Int and an operation that is a lambda expression (Int, Int) -> Int. The calculate function calls the operation with the given parameters a and b and returns the result.

You can input any lambda expression that matches the (Int, Int) -> Int signature into the calculate function, which allows you to perform different operations on the input values.

A function like calculate that takes another function as an argument is known as a higher-order function. Higher-order functions are a powerful feature of Kotlin that allows you to write more flexible and reusable code.

Classes

Classes are the building blocks of object-oriented programming. They are a system for organizing and reusing code. Think of a class as a blueprint for an object. It defines the properties and behaviors of the object, but it is not the object itself. When you create an object from a class, you are creating an instance of that class. The object has its own unique properties and behaviors, but it is based on the blueprint defined by the class.

Defining a Class

To define a class in Kotlin, you use the class keyword followed by the class name. Here is an example of a simple class definition:

class Person {
    var name: String = ""
    var age: Int = 0
}

In this example, we define a class called Person with two properties: name and age. The properties are defined using the var keyword, which indicates that they are mutable (can be changed). The properties are initialized with default values of an empty string and zero, respectively.

Creating Objects

To create an object from a class, you use the class name almost like a function call. Here is an example of creating a Person object:

val person = Person()

In this example, we create a Person object and assign it to a variable called person. The object is created using the default constructor of the Person class, which does not take any arguments. We will learn more about constructors in the next lesson.

Properties / Fields

Properties are variables that are associated with an object. They define the state of the object and can be accessed and modified from outside the class. In the Person class, name and age are properties. These are unique to each instance of the class. For example, you can create multiple Person objects with different names and ages.

Accessing Properties

You can access the properties of an object using the dot notation. Here is an example of accessing the name property of a Person object:

val person = Person()
person.name = "Alice"
println(person.name) // Output: Alice

In this example, we create a Person object and assign the name "Alice" to the name property. We then print the value of the name property using the println function.

Changing Properties

You can change the value of a property by assigning a new value to it. Here is an example of changing the age property of a Person object:

val person = Person()
person.age = 30
println(person.age) // Output: 30

In this example, we create a Person object and assign the age 30 to the age property. We then print the value of the age property using the println function.

Getters and Setters

In Kotlin, you can define custom behavior for getting and setting properties using getters and setters. Getters are methods that are called when you access a property, and setters are methods that are called when you assign a value to a property.

Here is an example of defining custom getters and setters for the name property of the Person class:

class Person {
    var name: String = ""
        get() = field.toUpperCase()
        set(value) {
            field = value.trim()
        }
}

In this example, we define a custom getter for the name property that converts the name to uppercase. We also define a custom setter that trims leading and trailing whitespace from the name. When you access or assign a value to the name property, the custom getter and setter are called.

Property Visibility

By default, properties in Kotlin are public, which means they can be accessed and modified from outside the class. You can also specify the visibility of properties using access modifiers. The available access modifiers are public, protected, private, and internal. But you mainly use public and private, so we will focus on those.

  • public: The property is accessible from anywhere.
  • private: The property is accessible only from within the class.

Both are pretty simple to understand. If you want a property to be accessible from outside the class, you make it public. If you want a property to be accessible only from within the class, you make it private.

Here is an example of a private property in the Person class:

class Person {
    private var ssn: String = ""
}

fun main() {
    val person = Person()
    // person.ssn = "123-45-6789" // Error: Cannot access 'ssn': it is private in 'Person'
}

In this example, we define a private property called ssn in the Person class. The property is only accessible from within the Person class. If you try to access the ssn property from outside the class, you will get a compilation error.

Methods & Constructors

Methods are a special kind of function that are defined inside a class. They are used to define the behavior of an object. In Kotlin, methods are defined using the fun keyword followed by the method name. The method can take parameters and return a value. Here is an example of a method that takes two parameters and returns the sum of the two numbers:

class Calculator {
    fun add(a: Int, b: Int): Int {
        return a + b
    }
}

fun main() {
    val calculator = Calculator()
    val sum = calculator.add(5, 10)
    println(sum) // Output: 15
}

In this example, we define a class called Calculator with a method called add. The add method takes two parameters a and b of type Int and returns the sum of the two numbers. We then create an instance of the Calculator class and call the add method with the values 5 and 10. The result is stored in the sum variable and printed to the console.

Constructors

Constructors are special methods that are called when an object is created. They are used to initialize the object with default values or values passed as arguments. In Kotlin, the primary constructor is defined in the class header, and code is ran using them in the init block. Here is an example of a class with a primary constructor:

class Person(name: String, age: Int) {
    init {
        println("Name: $name")
        println("Age: $age")
    }
}

But now we have a problem. We can't access the name and age properties outside of the init block. To fix this, we can define properties in the class header and initialize them using the primary constructor, the same way you can declare normal properties:

class Person(val name: String, val age: Int) {
    init {
        println("Name: $name")
        println("Age: $age")
    }
}

So now we can access the name and age properties outside of the init block. Here is an example of creating a Person object with a name and age:

fun main() {
    val person = Person("Alice", 30) // Output: Name: Alice
                                     //         Age: 30
    println(person.name) // Output: Alice
    println(person.age) // Output: 30
}

In this example, we create a Person object with the name "Alice" and age 30. We then print the name and age properties of the person object using the println function.

Secondary Constructors

In addition to the primary constructor, you can define secondary constructors in a class. Secondary constructors are defined using the constructor keyword and can have different parameter lists than the primary constructor. Here is an example of a class with a secondary constructor:

class Person {
    var name: String = ""
    var age: Int = 0

    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

In this example, we define a class called Person with two properties name and age. We then define a secondary constructor that takes a name and age parameter and assigns them to the properties. This also uses the this keyword which is used to refer to the current object from within itself, as you can not declare properties within the parameters of a secondary constructor.

Unit 1 Project: Movie Collection

In this project, you will create a simple movie collection program. The program will allow you to add movies to a collection, view the movies in the collection, and search for movies by title.

Project Requirements

  1. Create a Movie class with the following properties:

    • title: The title of the movie
    • year: The year the movie was released
    • genre: The genre of the movie
    • rating: The rating of the movie The Movie class should have a primary constructor that initializes these properties.
    • The properties should be read-only (use val instead of var).
    • The rating property should be a Double value between 0.0 and 10.0.
    • The year property should be an Int value representing the release year.
    • The genre property should be a String value representing the genre of the movie.
    • The title property should be a String value representing the title of the movie.
    • A display method that returns a string representation of the movie in the format: title (year) - genre - rating.
  2. Create a MovieCollection class to manage a collection of movies. The MovieCollection class should allow you to:

    • Add a movie to the collection.
    • View all movies in the collection.
    • Search for a movie by title.
    • Remove a movie from the collection by title.
    • Calculate the average rating of all movies in the collection.
    • Calculate the number of movies in the collection.
    • Clear the collection (remove all movies).
  3. Create a main function to test your MovieCollection class. In the main function, create an instance of the MovieCollection class and test the functionality of adding movies, viewing movies, searching for movies, removing movies, calculating the average rating, calculating the number of movies, and clearing the collection.

Obviously this isn't strictly "graded" but you should be able to demonstrate the functionality of your program to your software leads, as well as an understanding of the basic concepts of Kotlin.

It is recommended to use GitHub for this project, as you will need to submit it using github.

Once you have completed the project, send a message on Slack to your software leads with a link to the repository containing your project, so we can review your code.

Robot Programming

In this section we will learn about programming a FRC Robot with WPIlib.

Topics covered in this section include:

  • Intro to WPILIB (Units, Geometry Classes)
  • Device I/O
  • Robot Code Structure / Command Based Programming
  • PID Control
  • Feedforward Control

Intro to WPILIB and its Features

WPILIB is a library that provides a set of tools and utilities for programming robots in Java, C++, and Kotlin. It is the primary library used for programming FRC robots and provides a wide range of features to help you develop your robot code.

In this chapter, we will cover the basics of WPILIB and its utility features, including:

  • Typesafe Units
  • Geometry Classes
  • Math Utilities

All of these features are designed to make programming your robot easier and more efficient. Let's dive in!

Typesafe Units

One of the problems that can arise when programming things tha operate in the real world is that units can get mixed up. For example, if you are working with distances, you might accidentally mix up inches and meters. This can lead to bugs in your code that are difficult to track down.

WPILIB provides a set of typesafe units that help you avoid these issues. By "typesafe", we mean that different types of units like distances, angles, and velocities are represented by different classes. This makes it impossible to mix up units in your code.

The basics of using them is as follows:

val distance: Measure<Distance> = Units.Feet.of(10.0) // 10 feet

val angle: Measure<Angle> = Units.Degrees.of(90.0) // 90 degrees

val velocity: Measure<Velocity<Distance>> = Units.FeetPerSecond.of(5.0) // 5 feet per second

As you can see all units are a form of Measure with a specified type. This makes it easy to work with different units. Then, you can use the of method to create a new instance from a value.

There's a lot of other interesting things you can do with units, but this is the basic idea. To learn more about units, you can check out the WPILIB documentation.

Geometry Classes

WPILIB provides a set of classes that allow you to represent things like points, rotations, and poses in 2D and 3D space. These classes are useful for working with robot geometry and kinematics, and tie in nicely with the typesafe units we just talked about.

The table below shows some of the classes that WPILIB provides:

ClassDescription
Translation2dRepresents a 2D point with x and y coordinates.
Rotation2dRepresents a 2D rotation using a point on the unit circle
Pose2dRepresents a 2D pose with a translation and rotation.
Translation3dRepresents a 3D point with x, y, and z coordinates.
Rotation3dRepresents a 3D rotation using a quaternion.
Pose3dRepresents a 3D pose with a translation and rotation.

These classes are useful for representing things like robot positions and orientations, and can be used in conjunction with the typesafe units we talked about earlier.

You can learn more about these classes in the WPILIB documentation.

Math Utilities

At the moment WPILIB only really provides one math utility, and its made obsolete in a lot of cases because of the Typesafe Units. It provides a way to convert between different units stored as doubles, but you should use the typesafe units whenever possible.

Most of these methods are formatted as unit1ToUnit2 and are pretty self-explanatory. For example, to convert feet to meters, you would use feetToMeters and pass in a double.

Although there isn't a real page for this in the WPILIB documentation, you can find the methods in the WPILIB JavaDocs.

Device I/O

In this section, we will cover the basics of device I/O in WPILib. This includes how to interact with motors, sensors, and other devices on the robot.

Types of Communication

There are a few different ways that devices can communicate with the RoboRIO. The most common ones are:

  • DIO (Digital Input/Output)
  • PWM (Pulse Width Modulation)
  • CAN (Controller Area Network)

DIO (Digital Input/Output)

DIO communicates with devices using binary signals (on/off). This is useful for things like limit switches, buttons, and simple sensors.

PWM (Pulse Width Modulation)

PWM is a way to control the speed of motors and other devices by varying the width of a pulse. This is useful for controlling things like simple motors, servos, and picking up signals form some forms of encoders.

The "width" of the pulse refers to the amount of time the signal is high (on) compared to the total period of the signal. For example, a 50% duty cycle means the signal is high for half the time and low for the other half.

CAN (Controller Area Network)

CAN is a more advanced communication protocol that allows devices to communicate with each other over a network. This is useful for more complex systems that require high-speed communication between devices. Each device on the network has a unique ID that allows it to be addressed individually.

Inputs

DI (Digital Input)

The most basic form of input is a digital input. This is a simple on/off signal that can be used to detect things like limit switches, buttons, and other binary signals. Most DI devices will be connected to the RoboRIO's DIO ports. To interact with a DI device, you can use the DigitalInput class. Here's an example:

val limitSwitch = DigitalInput(0) // Connected to DIO port labeled 0 on the RoboRIO
val isPressed = limitSwitch.get()

Encoders

An encoder is a device that measures the rotation of a shaft. This is useful for measuring distances, velocities, and angles. Encoders can be connected to the RoboRIO's DIO ports, but usually the ones we use are integrated into the motor itself. We'll talk about motors later in this section. But for now, lets go over the different types of encoders we use:

NameTypeDescription
NEO EncoderRelativeIntegrated into NEO motors. Sends signals over CAN
REV Through Bore EncoderAbsolutePlaced around the output shaft of a mechanism. Sends signals using Duty Cycle / PWM
Kraken / Falcon EncoderRelativeIntegrated into Kraken and Falcon motors. Sends signals over CAN
CTRE CANcoderAbsoluteUses a magnet placed inside of a shaft to determine the absolute position of it. Sends signals over CAN

The most important one to understand is the NEO Encoder, as it is the most common one we use. The NEO Encoder is integrated into NEO motors and sends signals over CAN. This means that you can interact with it through a CANSparkMax, we'll go over how to create them later, but once created this is how you utilize the encoder

val neoMotor = CANSparkMax(0, MotorType.kBrushless)

val encoder = neoMotor.encoder // Get the encoder from the motor

val position = encoder.position // Get the current position of the motor
val velocity = encoder.velocity // Get the current velocity of the motor

Remember that the NEO Encoder is relative, meaning that it measures the position of the motor relative to where it was when it was turned on (or last reset). This is different from absolute encoders, which measure the position of the motor relative to a known starting point, that persists even when the motor is turned off.

Conversion Factors

By default the NEO Encoder measures position and velocity in Rotations and Rotations per Minute (RPM) respectively. If you need to convert these values to different scales, like when there's a gear ratio involved, you can modify positionConversionFactor and velocityConversionFactor fields on the encoder object.

The formula is rotor * conversionFactor, this means your conversion factors should always be 1/gearRatio, like so:

val encoder = neoMotor.encoder

// Set the conversion factor for position when using a 10:1 gear ratio
encoder.positionConversionFactor = 1.0 / 10.0 

// Set the conversion factor for velocity for a 150:7 gear ratio
encoder.velocityConversionFactor = 1.0/(150/7)

Gyroscopes

A gyroscope is a device that measures the orientation of an object. This is useful for things like driving straight, turning, and keeping track of the robot's position on the field. There are a TON of options for gyroscopes, but the most common one we use is called the Pigeon2, so that's what I'll show you how to use.

The pigeon uses a system called "StatusSignals" which is very complex so we won't go too deeply into it, but you can read about it HERE.

That being said, here's how you would use the pigeon:

val pigeon = Pigeon2(0) // Device has a CAN ID of 0

val angleSignal = pigeon.getYaw() // StatusSignal that represents the yaw (heading) of the robot
val angle = angleSignal.value // Get the actual value of the signal

// You can also do it all in one line, although that can have some performance implications
val angle = pigeon.getYaw().value

Outputs

Motors

Motors are devices that convert electrical energy into mechanical energy. This is what makes the robot move! There are many different types of motors, but the most common ones we use are NEOs and Krakens. Krakens are a bit more complex so we won't go over them here, and instead we'll focus on NEOs.

NEOs are brushless motors that are controlled over CAN. This means that you can interact with them through a CANSparkMax object in your code like we saw in the encoder example. Here's how you would create and use a NEO motor:

val neoMotor = CANSparkMax(0, MotorType.kBrushless) // Create a new NEO motor on CAN ID 0

Super simple! You have lots of options for controlling the motor, but the most common ones are set and setVoltage. set sets the speed of the motor as a percentage of its maximum speed, while setVoltage sets the voltage of the motor directly, and is therefore usually more repeatable and accurate. Here's how you would use them:

neoMotor.set(0.5) // Set the motor to 50% speed

neoMotor.setVoltage(6.0) // Set the motor to 6 volts

Solenoids / Pistons

Solenoids use pressurized air to create linear motion. We tend not to use them too much, but they are useful for things like quick actuation of mechanisms. Solenoids are controlled using the Solenoid or DoubleSolenoid classes.

The Solenoid class is used for single-acting solenoids where applying pressure makes them extend, and when you remove pressure a spring retracts them. The DoubleSolenoid class is used for double-acting solenoids where applying pressure makes them extend and applying pressure to the other port makes them retract, these do not have springs.

Here's how you would use them:

Single-Acting Solenoid

val solenoid = Solenoid(PneumaticsModuleType.REVPH, 0) // Create a new solenoid on the REV Pneumatics Hub module, with channel 0

solenoid.set(true) // Extend the solenoid
solenoid.set(false) // Retract the solenoid

Double-Acting Solenoid

val solenoid = DoubleSolenoid(PneumaticsModuleType.REVPH, 0, 1) // Create a new double solenoid on the REV Pneumatics Hub module with a forward channel of 0 and a reverse channel of 1

solenoid.set(DoubleSolenoid.Value.kForward) // Extend the solenoid
solenoid.set(DoubleSolenoid.Value.kReverse) // Retract the solenoid
solenoid.set(DoubleSolenoid.Value.kOff) // Turn off the solenoid

// You can also use the `toggle` method to toggle the solenoid between forward and reverse
solenoid.toggle()

// And you can use the `get` method to get the current state of the solenoid
val state = solenoid.get()

// The state will be one of the following:
// - DoubleSolenoid.Value.kForward
// - DoubleSolenoid.Value.kReverse
// - DoubleSolenoid.Value.kOff

Device Configuration with Apply

When you create a device object, you can configure its settings using the apply function. This is a kotlin function that allows you to attach a block of code to an object, this makes our code cleaner and easier to read. Here's an example:

val neoMotor = CANSparkMax(0, MotorType.kBrushless).apply {
    idleMode = IdleMode.kBrake
    setSmartCurrentLimit(40)
    setInverted(false)
}

As you can see you do not need this or neoMotor to access the object, you can just use the properties and methods directly.

Controllers

Controllers are devices that allow you to interact with the robot. This includes things like joysticks, gamepads, and other input devices. Controllers are connected to the computer running the driver station software, and the driver station software sends the input from the controllers to the robot over a network connection.

To interact with controllers in your code, you can use the Joystick or XboxController classes. Here's an example of how you would use them:

val joystick = Joystick(0) // Create a new joystick on USB port 0

val xAxis = joystick.getX() // Get the x-axis value of the joystick
val yAxis = joystick.getY() // Get the y-axis value of the joystick

val button1 = joystick.getRawButton(1) // Get the state of button 1 on the joystick
val controller = XboxController(0) // Create a new Xbox controller on USB port 0

val leftX = controller.leftX // Get the x-axis value of the left stick
val leftY = controller.leftY // Get the y-axis value of the left stick

val rightX = controller.rightX // Get the x-axis value of the right stick
val rightY = controller.rightY // Get the y-axis value of the right stick

val aButton = controller.aButton // Get the state of the A button on the controller

Controllers are a great way to interact with the robot and control its behavior. You can use them to drive the robot, control mechanisms, and trigger actions based on user input.

Conclusion

That's it for device I/O! You should now have a basic understanding of how to interact with motors, sensors, and other devices on the robot. If you have any questions, check the following resources:

IO Layers

IO Layers are a fundamental concept when using the AdvantageKit library. AdvantageKit is primarily used to provide abstract interfaces for controlling groups of hardware, which can then be used to easily log and simulate said hardware.

What is an IO Layer?

An IO Layer is a class that exposes a set of methods for interacting with a specific piece or group of hardware. For example, a Gripper IO Layer might expose methods for opening and closing the gripper, while a DriveTrain IO Layer might expose methods for driving the robot.

IO Layers are designed to be simple and easy to use, and are intended to abstract away the complexities of the hardware they control. This makes it easy to write code that interacts with the hardware, without having to worry about the specifics of how the hardware works.

Creating an IO Layer

Creating an IO Layer involves two steps, creating the IO Layer interface, which is basically a list of methods that the IO Layer will expose, and than as many IO Layer implementations as you need, which are the classes that actually implement the methods, but for different sets of hardware, or on a more basic level, real vs simulated hardware.

The interface also contains a nested Inputs class, which contains a list of values that the IO Layer will read from the hardware. These values then get logged and can be used for match replays, and similar things.

Creating the Interface

The interface is a simple Kotlin interface, with a nested Inputs class. Here is an example of a simple IO Layer interface for a Gripper:

interface GripperIO { // Define the interface for the Gripper
    class GripperIOInputs : LoggableInputs { // Define the inputs class
        var isOpen: Boolean = false // Define an input for the gripper state
        var holdingCube: Boolean = false // Define an input for whether the gripper is holding a cube
        var cubeDistance: Double = 0.0 // Define an input for the distance of the cube from the back of the gripper

        override fun toLog(table: LogTable?) {
            table?.put("isOpen", isOpen)
            table?.put("holdingCube", holdingCube)
            table?.put("cubeDistance", cubeDistance)
        }

        override fun fromLog(table: LogTable?) {
            isOpen = table?.get("isOpen") ?: isOpen
            holdingCube = table?.get("holdingCube") ?: holdingCube
            cubeDistance = table?.get("cubeDistance") ?: cubeDistance
        }
    }

    fun updateInputs(inputs: GripperIOInputs) {} // Define a method for updating the inputs of the gripper
    fun openGripper() {} // Define a method for opening the gripper
    fun closeGripper() {} // Define a method for closing the gripper
}

As you can see, it's a very simple concept. You may also notice the LoggableInputs interface, which is a simple interface that defines methods for converting the inputs to and from a LogTable, which is a simple key-value store that is used for logging. Note the curly braces after each method, this is required for the default implementation of the IO layer to be used for replay.

Input Classes In Depth

The Inputs class is used to "Log" the state of the mechanism at a given point in time. This allows us to do things like look at the state of our code when an error occurred, or even replay a match with code changes, using the original inputs.

The inputs class is comprised of 3 parts, the variables, the toLog method, and the fromLog method. The variables are the values that you want to log, and are the values that you will be updating in the IO layer's updateInputs method. The toLog method is used to convert the inputs to a LogTable, which is a simple key-value store that is used for logging the state of the mechanism to a file. The fromLog method is used to convert the LogTable back into the inputs, so that you can use them in your code.

The purpose of these inputs classes is to sit in between the hardware and the control logic, and log anything that is input into the control logic. Anything that is a output of the control logic, such as desired motor speeds, should not be logged in the inputs class. We will go over how to log those at a later date. Here are some examples of what would be inputs, and what would be outputs:

InputOutput
Anything Read From A Sensor or Device (Motor voltage, Position, Limit Switch StateDesired Motor Speed
If devices are enabled or disabledPose of the robot
If a mechanism is holding a game piece

There are a lot of things that can be inputs, and a lot of things that can be outputs, but the general rule of thumb is that if it's something that is read from a sensor or device, it should be an input, and if it's something that is calculated by the control logic, it should be an output.

Creating the Implementation

Now that you've defined the interface, you need to create an implementation of the interface. Here is an example of a simple implementation of the GripperIO interface using solenoids:

class GripperIOSolenoids : GripperIO {
    val solenoid = DoubleSolenoid(0) // Create a new Solenoid on port 0
    val sensor = Ultrasonic(0, 1) // Create a new Ultrasonic sensor on ports 0 and 1

    override fun updateInputs(inputs: GripperIOInputs) {
        inputs.isOpen = solenoid.get() == Value.kForward
        inputs.holdingCube = sensor.getRangeMM < 100
        inputs.cubeDistance = sensor.getRangeInches
    }

    override fun openGripper() {
        solenoid.set(Value.kForward)
    }

    override fun closeGripper() {
        solenoid.set(Value.kReverse)
    }

}

This implementation uses a DoubleSolenoid to control the gripper, and an Ultrasonic sensor to detect if a cube is present in the gripper. The updateInputs method reads the state of the solenoid and the distance of the cube from the back of the gripper, and updates the inputs accordingly.

You may notice that we're not exposing any methods for reading the inputs, this is because when using an IO layer, the inputs will be created elsewhere, and when the updateInputs method is called, it will update the inputs with the current state of the hardware, so then you can read the inputs from the inputs object.

Creating Inputs out of Measures (Units Library)

WPILib's java units library doesn't translate perfectly to AdvantageKit's inputs like doubles do, so let's real quick go over how you create your toLog and fromLog methods when using Measures.

interface FlywheelIO {
    class FlywheelIOInputs : LoggableInputs {
        var speed: MutableMeasure<Velocity<Angle>> = MutableMeasure.zero(Units.RadiansPerSecond)
        var voltage: MutableMeasure<Voltage> = MutableMeasure.zero(Units.Volts)

        // Luckily, you can just put the measure directly into the table
        override fun toLog(table: LogTable?) {
            table?.put("speed", speed) 
            table?.put("voltage", voltage)
        }
        
        // This is where it gets a bit more complicated
        override fun fromLog(table: LogTable?) {
            speed.mut_replace(table?.get("speed", speed)) // Replace the speed measure with the one from the table, or keep the old one if it can't be found
            voltage.mut_replace(table?.get("voltage", voltage)) // Replace the voltage measure with the one from the table, or keep the old one if it can't be found
        }
    }
    
    fun updateInputs(inputs: FlywheelIOInputs)
    fun setVoltage(voltage: Measure<Voltage>)
}

Your updateInputs also looks slightly different, so lets quickly go over that as well for completeness.

class FlywheelIOSpeedController : FlywheelIO {
    val motor = CANSparkMax(0, MotorType.kBrushless) // Create a new SparkMax on port 0
    
    override fun updateInputs(inputs: FlywheelIOInputs) {
        inputs.speed.mut_replace(motor.encoder.velocity, Units.RPM) // Assuming the encoder is outputting RPM velocity readings
        inputs.voltage.mut_replace(motor.appliedOutput * motor.busVoltage, Units.Volts)
    }
    
    override fun setVoltage(voltage: Measure<Voltage>) {
        motor.setVoltage(voltage into Units.Volts)
    }
}

Conclusion

IO Layers are a powerful tool for abstracting away the complexities of hardware, and making it easy to write code that interacts with the hardware. By creating simple interfaces and implementations, you can easily control and log hardware in your robot code, without having to worry about the specifics of how the hardware works. This makes it easy to write clean, maintainable code that is easy to understand and debug.

Additional Resources

Open-Loop Control

The last two modules in this unit cover the basics of "Control Systems" in robotics. Control systems are the algorithms that determine how a robot behaves in response to requested actions and sensor input. This first module covers "Open-Loop Control", which will be explained in the rest of the module.

What is Open-Loop Control?

As stated before, Control systems are the algorithms that determine how a robot behaves in response to requested actions and in some cases sensor input.

Open-Loop control systems are the simplest form of control systems. They are called "Open-Loop" because they do not actually utilize any sensor feedback to update the requested actions. Instead, the robot simply executes the requested actions without any regard to the environment or the robot's current state, assuming that the robot will always behave the same way given the same requested actions.

This is a very simple and straightforward way to control a robot, but it is also very limited. Open-Loop control systems are not able to adapt to changing conditions or unexpected events, and are generally not very reliable. However, they are useful in many cases in FRC, where precise control is not necessary.

Duty Cycle / Voltage Control

These are the simplest forms of Open-Loop control. They simply invole telling a motor to run at a certain percentage of its maximum speed, or to run at a certain voltage. This is the most basic form of control, but they come with several drawbacks.

Duty Cycle

Duty Cycle control involves telling a motor to run at a certain percentage of its maximum speed. The downside to this is that the actual speed of the motor can vary greatly depending on the load on the motor. For example, if the motor is under a heavy load, it may not be able to reach the requested speed, and if the motor is under a light load, it may run faster than requested.

Voltage Control

Voltage control is similar to Duty Cycle control, but instead of telling the motor to run at a certain percentage of its maximum speed, you tell it to run at a certain voltage. This is slightly more accurate than Duty Cycle control, and is much more repeatable in terms of consistent speeds. The major downside of this is that that concistency can be thrown off by the battery voltage, which can vary greatly depending on the charge of the battery.

Implemation Example

Voltage and Duty Cycle control can be utilized with SparkMax motor controllers using .set() and .setVoltage() methods.

val motor = CANSparkMax(1, MotorType.kBrushless)
motor.set(0.5) // Run the motor at 50% speed
motor.setVoltage(6.0) // Run the motor at 6 volts

Very simple, right? These methods are very easy to use, but they are not very reliable. They are useful for simple tasks where precision is not necessary, but they are not recommended for more complex tasks.

Feedforward Control

Feedforward control is a much more advanced form of Open-Loop control, and is in fact the main form of control used for a lot of systems in FRC. Feedforward control is really a fancy name for a function that takes in a velocity, and returns a voltage that will make the motor run at that velocity.

You may be wondering "How is this different from Voltage Control?" The difference is that Feedforward control takes into account the characteristics of the motor and the system it is controlling, and can adjust the voltage based on that. For example, the feedforward functions take into account the amount of voltage required to overcome the static friction of the motor.

The Math

The basic formula for Feedforward control is as follows: \( V(x) = ( K_v * x) + ( K_a * \dot x ) + ( K_s * sign( x) ) \)

Now, let's break down what each of these terms mean:

  • \( V(x) \) is the voltage that the motor should run at to achieve the desired velocity \( x \).
  • \( K_v \) is the velocity gain, which is the amount of voltage required to make the motor run at a certain velocity, and is usually measured in volts per meter per second.
  • \( K_a \) is the acceleration gain, which is the amount of voltage required to make the motor accelerate at a certain rate, and is usually measured in volts per meter per second squared.
  • \( K_s \) is the static friction gain, which is the amount of voltage required to overcome the static friction of the motor, and is usually measured in volts.
  • \( x \) is the desired velocity of the motor as stated before.
  • \( \dot x \) is the desired acceleration of the motor.
  • \( sign(x) \) is the sign of the velocity, which is used to determine the direction of the motor, and therefore the direction of the voltage.

This formula is a very simple equation on the surface, but it can be very powerful when used correctly. It allows you to control the motor in a very precise way, and can be used to make the motor behave in a very specific way.

Downside of Feedforward Control

Like all Open-Loop control systems, Feedforward control is not perfect. It is not able to adapt to changing conditions or correct for errors in the system. This means that if the system is not tuned correctly, the motor may not behave as expected, and may not be able to achieve the desired velocity.

How to find the gains

For simple motor feedforward, it's very easy to find K_v, and K_s. K_a is a lot harder to find, but luckily, it's not usually necessary to tune it.

To find your K_v and K_s you use the following steps:

  • Run the motor at several different voltages, and measure the velocity of the motor at each voltage.
  • Plot the voltage as a function of the velocity, and find the slope of the line. This is your K_v.
  • Find the y-intercept of the line. This is your K_s.

Implementation Example

WPIlib provides a SimpleMotorFeedforward class that can be used to implement feedforward control. Remember, feedforward outputs a voltage, so you need to use .setVoltage() to set the motor to the desired voltage, rather than .set().

val motor = CANSparkMax(1, MotorType.kBrushless)
val feedforward = SimpleMotorFeedforward(0.1, 0.2, 0.05) // K_v, K_a, K_s

val velocity = 5.0 // Desired velocity in meters per second
val acceleration = 2.0 // Desired acceleration in meters per second squared

val voltage = feedforward.calculate(velocity, acceleration)
motor.setVoltage(voltage)

This is a very simple example of how to use feedforward control. It is a very powerful tool, and can be used to control the motor in a very precise way. Feedforward can also be used to take gravity and other external forces into account, but those will not be covered, and if you are interested, we will provide resources for you to learn more.

Conclusion

Open-Loop control is the simplest form of control systems, and is not able to adapt to changing conditions or correct for errors in the system. It is useful for simple tasks where precision is not necessary, but is not recommended for more complex tasks.

Duty Cycle and Voltage control are the simplest forms of Open-Loop control, and are not very reliable. They are useful for simple tasks where precision is not necessary, but are not recommended for more complex tasks.

Feedforward control is a much more advanced form of Open-Loop control, and is in fact the main form of control used for a lot of systems in FRC. It is a very powerful tool, and can be used to control the motor in a very precise way.

Additional Resources

Closed-Loop Control

Now that we've covered Open-Loop control, we can move on to Closed-Loop control. Closed-Loop control is on a basic level pretty simple to understand, but it can get complicated (and interesting) pretty fast. The main idea behind Closed-Loop control is that rather than output being based on purely the input, the output is based on the input, as well as the measured distance between the output and input. This allows the system to correct for errors in the system, and adapt to changing conditions.

Proportional Control

Proportional control is the simplest form of Closed-Loop control, and is the most common form of Closed-Loop control used in FRC. Proportional control simply takes the distance (or error) between the desired value and the actual value, and multiplies it by a constant, called the Proportional Gain or K_p. This value is then output to the motor.

Alternatively, rather than simply outputting that value, you can add it to the output of an Open-Loop control system, such as Feedforward control. This is incredibly useful as pure Proportional control is good at error correction, but it can be difficult to find a K_p that both quickly reaches the target, but doesn't overshoot it. By using Feedforward control to get close to the target, and Proportional control to correct for errors, you can get the best of both worlds.

Implementing Proportional Control

Lucky for us, WPILib has a built-in class that can be used to implement Proportional control, called PIDController. This class can be used to implement other forms of Closed-Loop control as well, but we'll get to that later.

val K_p = 0.1
val motor = CANSparkMax(1, MotorType.kBrushless)

val controller: PIDController =
    PIDController(K_p, 0.0, 0.0) // Create a new PIDController with a K_p of 0.1, for now ignore the other values
controller.setSetpoint(100.0) // Set the desired position of the motor to 100 rotations

// Placeholder function for something getting called periodically
fun periodic() {
    val output =
        controller.calculate(motor.encoder.position) // Calculate the output of the PIDController based on the current position of the motor, and the desired position
    motor.setVoltage(output) // Set the motor to the output of the PIDController
}

As you can see, implementing Proportional control is pretty simple. The PIDController class takes care of all the math for you, and all you need to do is call the calculate() function with the current position of the motor, and the desired position. The setVoltage() function is used to set the motor to the output of the PIDController.

Implementing Feedforward and Proportional Control

Implementing Feedforward and Proportional control is also pretty simple. All you need to do is add the output of the PIDController to the output of the SimpleMotorFeedforward class.

val K_p = 0.1
val motor = CANSparkMax(1, MotorType.kBrushless)

val feedforward = SimpleMotorFeedforward(0.1, 0.2, 0.05) // K_v, K_a, K_s
val controller: PIDController =
    PIDController(K_p, 0.0, 0.0) // Create a new PIDController with a K_p of 0.1, for now ignore the other values
controller.setSetpoint(100.0) // Set the desired velocity of the motor to 100 rotations per minute

// Placeholder function for something getting called periodically
fun periodic() {
    val output =
        feedforward.calculate(controller.setpoint) + controller.calculate(motor.encoder.position) // Calculate the output of the PIDController based on the current position of the motor, and the desired position
    motor.setVoltage(output) // Set the motor to the output of the PIDController
}

As you can see, implementing Feedforward and Proportional control is pretty simple. All you need to do is add the output of the PIDController to the output of the SimpleMotorFeedforward class, and set the motor to the output of that calculation.

Implementing Proportional Control and Feedforward Control on a SparkMax

SparkMaxs have a built-in PID controller that can be used to implement Proportional control. This is a lot simpler than using the PIDController class, as the SparkMax takes care of all the math for you.

val K_p = 0.1
val motor = CANSparkMax(1, MotorType.kBrushless)

val controller = motor.pidController // Get the built-in PID controller of the SparkMax

controller.p = K_p // Set the Proportional gain of the PID controller to 0.1
controller.setReference(100.0, ControlType.kPosition) // Set the desired position of the motor to 100 rotations

As you can see, implementing Proportional control on a SparkMax is pretty simple. All you need to do is set the Proportional gain of the built-in PID controller, and set the desired position of the motor. Unlike with the PIDController class, you only need to set the setpoint once, and the SparkMax will take care of the rest.

Feedforward control can be added to the setReference() function by adding some extra parameters.

val K_p = 0.1
val motor = CANSparkMax(1, MotorType.kBrushless)

val feedforward = SimpleMotorFeedforward(0.1, 0.2, 0.05) // K_v, K_a, K_s
val controller = motor.pidController // Get the built-in PID controller of the SparkMax

controller.p = K_p // Set the Proportional gain of the PID controller to 0.1

controller.setReference(
    100.0,
    ControlType.kVelocity,
    0, // Slot 0 (ignore this, just needed to add feedforward)
    feedforward.calculate(
        100.0,
        0.0
    ), // Calculate the feedforward output based on the desired velocity and acceleration
    ArbFFUnits.kVoltage // Set the units of the arbitrary feedforward to volts
)

As you can see, adding Feedforward control to the SparkMax is pretty simple. All you need to do is calculate the output of the SimpleMotorFeedforward class, and add it to the setReference() function. You also need to set the units of the arbitrary feedforward to volts, as the SparkMax expects the output of the feedforward to be in volts.

Unfortunately, the SparkMax's feedforward capabilities are limited, and require you to calculate the feedforward output on the RoboRIO, which means that we're limited in how fast we can run the feedforward loop.

What unit is K_p in?

The unit of K_p is similar to the units of the feedforward gains, where it is based on the units of the input and output. For example, if the input is in meters, and the output is in volts, then K_p would be in volts per meter. But rather than being volts per meter of the target, it is volts per meter of error (or distance from the target).

Conclusion

In this chapter, we covered the concept of Closed-Loop control, and how it can be used to correct for errors in the system. We specifically covered Proportional control, and how it can be used to correct for errors in the system. We also covered how to implement Proportional control using the PIDController class, and how to implement Proportional control on a SparkMax. We also covered how to implement Feedforward and Proportional control, and how to implement Feedforward and Proportional control on a SparkMax.

Additional Resources

The information below covers PID control, which is a more advanced form of Closed-Loop control. It is not necessary for basic parts of our robot, but if you want to be more involved in the complex and interesting parts of our robot, it is definitely worth looking into.

  • PID Control WPILib Docs - Comprehensive introduction to PID control concepts, including detailed explanations of proportional, integral, and derivative terms with practical examples of their effects on system behavior.
  • Combining Feedforward and PID Control - Explains how to effectively combine feedforward and PID control for better system performance, with examples showing how each component contributes to the final control output.
  • MatLab PID Video - Visual introduction to PID control using animated examples and real-world applications, making complex control concepts more approachable and intuitive.
  • WPILib's Flywheel Tuning Guide - Provides interactive simulations demonstrating different control strategies (bang-bang, feedforward, PID, and combined feedforward-PID) for tuning a flywheel velocity controller. The visual simulations make it easy to understand how different control parameters affect system behavior.

Robot Code & Robot Structure

Up until this point we've only really talked about controlling individual parts of a robot, but that isn't that useful is it? Luckily WPILIB provides a system of structuring your hardware control code that makes it easy to control larger parts of the robot like mechanisms, and more complex systems like a computer vision pipeline.

This system is called "Command Based Programming" and it's a way of organizing your code into subsystems and commands that interact with each other. This makes it easy to write code that controls the robot in a way that makes sense, and is easy to understand. It also provides some aspects of safety that we'll talk about later.

Suppliers

Although not directly related to Command Based Programming, it's important to understand the concept of Suppliers. When you pass a double to a method, you pass in that value at that time, this is fine for a lot of situations, but what about joysticks? Joysticks are constantly changing, so you need a way to get the value of the joystick every time you need it, without calling the method again, like in a loop within a command.

Suppliers store an action that can be called to get a value, and when you call the get() method on the supplier, it calls that action and returns the value. This is useful for things like joysticks, where you want to get the value of the joystick every time a loop runs within a function, rather than running that function in a loop.

Suppliers are super easy to declare, all you really need right now is double suppliers, which are suppliers that return a double. Here's an example of a supplier that returns the value of a joystick axis, DoubleSuppliers are unique in that they have a property called asDouble that returns the value of the supplier, rather than calling the get() method.

val joystick = Joystick(0)
val axisSupplier = DoubleSupplier { joystick.getRawAxis(0) }

println(axisSupplier.asDouble) // Prints the value of the joystick axis

Subsystems

The first part of Command Based Programming is Subsystems. Subsystems are classes that represent a part of the robot, like the drivetrain, or an arm mechanism. They contain the hardware objects that control that part of the robot, like motors, sensors, and solenoids.

Subsystems are intended to be used to store information (state) of a subsystem, as well as control access to the hardware to make sure that it is always only being given one command, and always being controlled in specific "allowed" ways.

Subsystems typically contain three parts

  • Properties: These are the hardware objects that control the subsystem. For example, a drivetrain subsystem might have two motors, one for the left side and one for the right side.
  • Control Methods: These are the functions that control the subsystem. For example, a drivetrain subsystem might have a method called drive that takes a speed and a rotation and drives the robot at that speed and rotation.
  • Command Factory Methods: These are methods that create commands that control the subsystem. For example, a drivetrain subsystem might have a method called createDriveCommand that creates a command that drives the robot at a certain speed and rotation.

Here's an example of a simple subsystem that controls a drivetrain

import java.util.function.DoubleSupplier

class Drivetrain : Subsystem() {
    private val leftMotor = CANSparkMax(0, MotorType.kBrushless)
    private val rightMotor = CANSparkMax(1, MotorType.kBrushless)

    fun drive(speed: Double, rotation: Double) {
        leftMotor.set(speed + rotation)
        rightMotor.set(speed - rotation)
    }

    fun createDriveCommand(speed: DoubleSupplier, rotation: DoubleSupplier): Command {
        // Note the use of suppliers, as whatever you place within the run method will be run every robot loop until its cancelled
        // We'll discuss this more later, just make sure you note the use of suppliers
        return this.run {
            drive(speed.asDouble, rotation.asDouble)
        }
    }
}

Commands

Commands are classes that control a subsystem. They contain the logic that controls the subsystem, like driving the robot, or moving an arm mechanism. The advantage of commands is that they stop you from controlling the subsystem in multiple places at once, which can cause bugs, or slamming the arm mechanism into the air filtration units.

Commands can be created in many ways, but as a rookie the important ones to know are the Subsystem factory methods, which are contained in the table below.

MethodDescription
runRuns a lambda every robot loop until the command is cancelled
runEndRuns one lambda every loop until command is cancelled, runs a second one one time when it ends
runOnceRuns a lambda one time when the command is started
startEndRuns a lambda one time when the command is started, runs a second one one time when it ends

By lambdas, we mean the code that is placed within the brackets of the method. For example, in the run method, the lambda is drive(speed.asDouble, rotation.asDouble). This lambda will be run every robot loop until the command is cancelled.

To create a command factory method, you want to create a method and use the factory methods shown above to create a command. running it on this will make sure that the command is associated with the subsystem, meaning no other commands can run on the subsystem at the same time.

Here's an example of a command that drives a robot at a certain speed and rotation

fun createDriveCommand(speed: DoubleSupplier, rotation: DoubleSupplier): Command {
    return this.run {
        drive(speed.asDouble, rotation.asDouble)
    }
}

Command Groups

Command Groups are classes that contain multiple commands that run in sequence. This is useful for complex actions that require multiple steps, like picking up a game piece, driving to a location, and scoring the game piece.

Command Groups are easiest to create using the method chaining API, which is a way of creating a command group by chaining methods together. This is done different ways depending on the type of command group, but the most common ones are sequential groups using .andThen() and parallel groups using .alongWith().

Here's an example of a command group that drives the robot forward for 3 seconds, then stops, turns 90 degrees, and drives forward for 3 more seconds

val driveForward = Drivetrain.createDriveCommand({ 0.5 }, { 0.0 }).withTimeout(3.0)
val turn = Drivetrain.createDriveCommand({ 0.0 }, { 0.5 }).withTimeout(1.0) // Assume 1s is 90 degrees

val group = driveForward.andThen(turn).andThen(driveForward)

Scheduling Commands & Triggers

Remember suppliers? They're back! This time, they're used to schedule commands. Triggers are basically fancy boolean suppliers that allow you to tie commands to certain conditions. For example, you could tie a command to a button press, or a button release, or anything else that can be represented with a boolean value!

Here's an example of a trigger that runs a command when a button is pressed

val joystick = Joystick(0)

val trigger = Trigger { joystick.getRawButton(1) }
val command = Drivetrain.createDriveCommand({ 0.5 }, { 0.0 }).withTimeout(3.0)

trigger.onTrue(command)

This code will run the command when button 1 on joystick 0 is pressed until 3 seconds have passed. Although this is kind of verbose isn't it? You can actually simplify this code to just use the CommandJoystick (and CommandXboxController) class, which is a joystick that has methods that return triggers directly, rather than booleans that you have to wrap in a trigger.

val joystick = CommandJoystick(0)

val command = Drivetrain.createDriveCommand({ 0.5 }, { 0.0 }).withTimeout(3.0)

joystick.button(1).onTrue(command)

This code does the same thing as the previous code, but it's a lot simpler and easier to understand.

Finally, you may notice that we've been using onTrue to run a command when a trigger is true, but what if you want to run a command based on other conditions? The Trigger JavaDocs contain information on all the different methods you can use to run commands based on different conditions, but the other two important ones are whileTrue which runs a command while a trigger is true but cancels it if its no longer true, and toggleOnTrue which runs a command when a trigger is true, but cancels it if the trigger is true again.

The Robot Class

Now that we've talked about commands, and triggers, lets talk about how we use them.

The Robot class is the main class of your robot code. It's responsible for initializing the subsystems and commands, as well as running the "Command Scheduler" which is the part of WPILIB that actually tracks and runs commands.

The Robot class inherits from TimedRobot, this gets it a init and periodic method for each of the robot modes ( disabled, autonomous, teleop, etc) as well as an overall robotPeriodic method that runs every robot loop, regardless of the mode. All periodic methods are run every 20ms, or 50 times a second, this is our "loop time".

You may notice the lack of a robotInit method, this is because the constructor is just as good for initializing the robot, and there's no reason to not just declare all of your commands within the Robot object.

Binding Commands in the Robot Class

The Robot class is where you bind commands to triggers. This is done by creating triggers and commands in the Robot class, and then binding them together using the onTrue, whileTrue, and toggleOnTrue methods as discussed earlier.

You can also bind commands to triggers in the Robot class using the CommandJoystick and CommandXboxController, extensions of the Joystick and XboxController classes that have methods that return triggers directly, instead of booleans that you have to wrap in a trigger. This can be done cleanly using the apply method like when initializing hardware objects.

Here's an example of binding a command to a button press in the Robot class

class Robot : TimedRobot() {
    private val controller = CommandXboxController(0).apply {
        b().onTrue(PrintCommand("Hello, World!"))
        b().onFalse(PrintCommand("Goodbye, World!"))
    }
}

as you can see, we've created a PrintCommand that prints "Hello, World!" when a button is pressed, and "Goodbye, World!" when the button is released. We then bind the command to the button press using the onTrue method.

Setting default commands

Often you will want your subsystems to have a default command that runs when no other command is running. This is done by setting the defaultCommand property of the subsystem to the command you want to run. Once again, this is done in the Robot class using the apply method.

Here's an example of setting a default command for a subsystem in the Robot class

class Robot : TimedRobot() {
    private val drivetrain = Drivetrain().apply {
        defaultCommand = Drivetrain.createDriveCommand({ 0.0 }, { 0.0 })
    }
}

Conclusion

Command Based Programming is a powerful way to structure your robot code. It makes it easy to write code that is organized, easy to understand, and safe. It also provides a way to control complex systems like mechanisms and computer vision pipelines.

We've only scratched the surface of Command Based Programming in this module. There's a lot more to learn, but this should give you a good starting point. If you want to learn more (and you should), you can check out the WPILIB docs as always. Some good starting points are the following

Subsystems With AdvantageKit

Building off of both the previous section, and the section on IO Layers, we can now create a subsystem that uses an IO layer to interact with hardware. This allows us to easily swap out the implementation of the hardware without changing the code that uses it, and makes it easy to test the subsystem in isolation with a mock implementation of the IO layer.

Creating a Subsystem

The structure of a subsystem using AdvantageKit is very similar to that of a subsystem using barebones WPILib. The main difference is that instead of directly interacting with the hardware, you interact with an IO layer that abstracts away the hardware. This makes it easy to swap out the implementation of the hardware without changing the code that uses it, and makes it easy to test the subsystem in isolation with a mock implementation of the IO layer.

A subsystem in AdvantageKit typically won't contain any methods for interacting with the hardware, instead, it will contain a public instance of an IO layer for raw hardware interaction from outside the subsystem, and command factory methods that create commands that interact with the hardware.

A subsystem in AdvantageKit also typically won't contain any properties for the sensor values, instead, it will contain a public instance of an Inputs object that is updated by the IO layer, and can be read from outside the subsystem.

Here's an example of a subsystem that controls a drivetrain using an IO layer:

/*
Assume we have a GripperIO interface that contains methods for opening and closing the gripper, and an Inputs class
that contains the sensor values for the gripper.
 */

class Gripper : SubsystemBase {
    val io = when (RobotBase.isReal()) {
        true -> RealGripperIO()
        false -> SimulatedGripperIO()
    }

    val inputs = GripperIO.GripperIOInputs()

    fun openGripper(): Command {
        return this.run {
            io.openGripper()
        }.until { inputs.isOpen }
    }

    fun closeGripper(): Command {
        return this.run {
            io.closeGripper()
        }.until { !inputs.isOpen }
    }

    fun grabCube(): Command {
        return Commands.sequence(
            openGripper(),
            Commands.waitUntil { inputs.cubeDistance < 0.1 },
            closeGripper()
        )
    }

    override fun periodic() {
        io.updateInputs(inputs)
        Logger.processInputs("gripper", inputs)
    }
}

In this example, the Gripper class contains an instance of a GripperIO interface that contains methods for opening and closing the gripper, and an instance of a GripperIOInputs object that contains the sensor values for the gripper. The Gripper class also contains command factories for opening and closing the gripper, and a method for grabbing a cube that uses the command factories to open and close the gripper based on the sensor values.

As you can also see, when creating the subsystem you'll need to decide which implementation of the IO layer to use. This is usually done by checking if the robot is running in simulation or not, and creating an instance of the real or simulated IO layer accordingly.

Logging Outputs

When creating a subsystem, you wont just want to log the inputs into your control logic, you'll also want to log the outputs of the subsystem. This is useful for debugging, and for tuning the control loops of the subsystem.

Some outputs could be the estimated pose of odometry, the estimated pose from vision, or the trajectory that the robot is following. These outputs can be logged easily using the Logger.recordOutput method, which takes a key and a value. This value can be a number, a boolean, a string, or a complex object that implements WPISerializable, such as a Pose2d or another geometry object.

The Logger.recordOutput method is called in the periodic method of the subsystem, as it must be called every loop iteration to log the outputs. Here's an example of how you could log the estimated pose of odometry in the DriveSubsystem:

class DriveSubsystem : SubsystemBase {
    
    val io = when (RobotBase.isReal()) {
        true -> RealDrivetrainIO()
        false -> SimulatedDrivetrainIO()
    }
    
    val inputs = DrivetrainIO.DrivetrainIOInputs()
    val odometry = Odometry() // Not how odometry is actually implemented, but you get the idea

    fun getDriveCommand(speed: Double, rotation: Double): Command {
        return this.run {
            io.drive(speed, rotation)
        }
    }

    fun driveDistance(distance: Double): Command {
        return this.run {
            io.drive(0.5, 0.0)
        }.until { inputs.leftWheelDistance >= distance }
    }

    override fun periodic() {
        io.updateInputs(inputs)
        Logger.processInputs(inputs)

        Logger.recordOutput("Estimated Pose", Pose2d.struct, odometry.estimatedPose)
    }
}

In this example, the DriveSubsystem class contains an instance of an Odometry class that estimates the pose of the robot, and the periodic method of the DriveSubsystem class logs the estimated pose of the robot using the Logger.recordOutput method.

You may also notice that we passed more than just a key and a value to the Logger.recordOutput method. The Pose2d class is a complex object that implements WPISerializable, and is used to serialize the Pose2d object into a format that can be logged. This is useful for logging complex objects that can't be logged directly, such as geometry objects or other complex objects. To do this you need to pass in the struct property of the complex object. This tells the logger to serialize the object into a format that can be logged.

Logging Arrays

Collections (Arrays, Lists, ETC) can also be logged using the Logger.recordOutput method. This is useful for logging arrays of sensor values, or other collections of values. To log an array, you do the same thing as logging any other value, but rather than just passing in the nane of the value you want to log, you add the * operator before the array name. This "unwraps" the array so AdvantageKit can process it properly.

Here's an example of how you could log an array of vision target poses in the DriveSubsystem:

class VisionSubsystem : SubsystemBase {
    val io = when (RobotBase.isReal()) {
        true -> RealVisionIO()
        false -> SimulatedVisionIO()
    }
    val inputs = VisionIO.VisionIOInputs()

    override fun periodic() {
        io.updateInputs(inputs)
        Logger.processInputs(inputs)

        Logger.recordOutput("Vision Targets", Pose2d.struct, *inputs.result.visionTargets)
    }
}

In this example, the VisionSubsystem class contains an instance of a VisionIO interface, and an instance of a VisionIOInputs object that contains the sensor values for the vision system. The VisionSubsystem class logs an array of vision target poses using the Logger.recordOutput method.

Conclusion

AdvantageKit is a very powerful library that allows you to easily test different combinations of hardware on your robot. The ability to create a subsystem decoupled from its hardware is incredibly powerful for things like simulation and logging in a very controlled manner.

The AdvantageKit GitHub repository has a ton of example projects and resources if you would like to look more into this, you can find this HERE

Team 2537 Development Practices Introduction

This document contains the general philosophy that our team has about developing robot code. Programming a competition robot's codebase is fundamentally a collaborative effort, and we have a few guidelines to help make that process as smooth as possible.

These guidelines are divided into two sections:

General Code Practices

When writing code, following certain practices helps you and others understand it better. While basic formatting like newlines is handled by IntelliJ/Spotless (our code formatter), there are additional important practices to keep in mind when writing code on our team.

Naming Conventions

  • Variables: Use descriptive names to help understand what the code is doing.
    • Example: distanceTraveled instead of x.
    • Use camelCase: The first word is lowercase, and every subsequent word is capitalized.
  • Classes: Use PascalCase: Every word is capitalized.
    • Example: RobotBase.

Comments

  • Use comments to explain why the code is doing something, not what it is doing. The code itself should be self-explanatory.

Code Structure

  • Organization: Group related code together. For example, place all code related to driving the robot in one section.

Subsystem Structure

  • Properties and Methods:
    • Place property-based data access (e.g., get() methods for small pieces of info) after init blocks and constructors but above methods.
    • Organize methods by functionality, grouping related methods together (e.g., all driving-related methods in one group).

Documentation

  • Document your code as you write it. This helps you and others understand your code and serves as a reference for future development.
  • JavaDocs/Dokka:
    • It is encouraged to write JavaDocs/Dokka while working, but it is not mandatory for small changes.
    • For larger pull requests, JavaDocs/Dokka implementation is required.

Code Review

  • Code review is crucial for catching bugs and improving code quality.
  • Have your code reviewed by at least one other person before merging it into the main branch.
  • Software leads and mentors have the final say on what gets merged, but peer reviews are highly encouraged.

Miscellaneous General Code Practices

  • Use property declaration in your constructor parameters.
  • Prefer val over var whenever possible.
  • Always use explicit types when declaring variables.
  • Avoid using magic booleans; instead, use enums and when statements if possible.

Misc Robot Code Practices

  • Motor Units should always be in the native units of the motor controller, use conversions and Measures to access other units IN CODE not on the motor controller.

Dokka / KDocs

Kotlin provides a very nice tool called Dokka that is used to generate a website that documents your code. This site is generated from specially formatted comments in your code, called KDocs. These comments are written in a special format that allows Dokka to understand them and generate a website from them.

Here's an example of a KDoc comment on a function:

/**
 * This is a KDoc comment. It is used to document your code.
 * 
 * @param x The x coordinate of the point.
 * @param y The y coordinate of the point.
 * @return The distance from the origin to the point.
 */
fun distanceFromOrigin(x: Double, y: Double): Double {
    return sqrt(x * x + y * y)
}

As you can see its a normal multi-line comment, but it starts with /** instead of /*. This is how Kotlin knows that this is a KDoc comment. Inside the comment, you can use special tags like @param to document the parameters of the function, and @return to document the return value of the function. These tags are used by Dokka to generate the documentation website.

You can also document classes, properties, and other things in Kotlin using KDocs. Here's an example of a KDoc comment on a class:

/**
 * This is a KDoc comment. It is used to document your code.
 * 
 * @property x The x coordinate of the point.
 * @property y The y coordinate of the point.
 * @constructor Creates a new Point with the given coordinates.
 */
class Point(val x: Double, val y: Double) {
    // Class implementation goes here
}

As you can see, you can use the @property tag to document the properties of the class, and the @constructor tag to document the constructor of the class. These tags are used by Dokka to generate the documentation website.

The rest of the tags you can use in KDocs, as well as more information on how to write KDocs, can be found in the KDocs documentation.

Version Control Standards

Our team uses GitHub.

We have a Github Organization (Team2537) where all of our robot code lives.

Each year, we create a new repository for the robot code. This repository is where all of the code for the robot lives. Here are some past years repositories:

We use branches within these repositories to manage different versions of the robot code and to develop features in parallel.

Special Branches

There are a few special branches that we use in our repositories:

  • main - This is the main branch of the repository. We plan to push code to this branch about weekly during the build season. This code should be fully tested and ready to deploy.
  • dev - This is the development branch. This is where we merge features and fix small bugs. When we are actually working on code in the workshop, changes will be done here. Code that is on the dev branch may not be fully tested, but should build and deploy successfully.
  • [competition] - Every competition that we participate in will have a branch. This branch will be created the night before we go to competition, and should include the contents of main (and maybe dev) at that time. Over the course of the competition, we may have to make emergency changes to the code. These changes will be made on the competition branch, and then possibly merged back into dev when we get back to the shop. The name of the competition branch should be the name of the competition, words separated by hyphens. For example, the branch for the CHS District Severn competition is CHS-district-severn.

Feature Branches

Every new feature that we develop should be developed on a feature branch. This branch should be named after the feature that is being developed. For example, if we are developing a launcher subsystem, we might create a branch called launcher-subsystem. When the feature is complete, we will merge the feature branch into the dev branch through a Pull Request. You may not merge directly to dev. Each new feature branch should be created from the dev branch.

Pull Requests

A pull request is a proposal to merge a set of changes from one branch into another. In a pull request, collaborators can review and discuss the proposed set of changes before they integrate the changes into the main codebase.

There are three types of Pull Requests that we use:

  • Feature Pull Request - This is a pull request that proposes to merge a feature branch into the dev branch. This pull request should be reviewed by at least one other team member before it is merged.
  • Dev Pull Request - This is a pull request that proposes to merge the dev branch into the main branch. These pull requests should be initialized and merged by both Software Leads. We plan to do this about weekly during the build season.

Please make sure your code follows the code practices before creating a pull request. If you do not follow the code practices, your code may not be merged.


We also use other version control features:

Commits

Each commit should be a small, atomic change. This means that each commit should only change one thing. This makes it easier to review changes and to understand what has been done. If you need to make multiple changes to implement a feature, you should make multiple commits.

WIP Commits

If you are working on a feature and you are still writing code (maybe mid-line or mid-function) you shouldn't commit an incomplete change. Instead, you should commit a "Work In Progress" (WIP) commit. This commit should be marked as a WIP commit by starting the commit message with WIP:. This will let other team members know that the change is not complete. When you are finished with the feature, you can squash the WIP commits into a single commit.

Tags

We don't use tags very often, but we may use them to mark important points in the codebase. For example, we might tag the autonomous code that we first get working on the robot with a tag like auto-working.

Issues

We use GitHub Issues to track absolutely everything. New feature? New issue. Bug? Issue. Need Documentation? Need Issue. Anyone can and should create issues on our robot's repository. For most Pull Requests, there will be an issue that the PR is addressing.

Projects!

We use GitHub Projects to track the progress of our robot code. Each project has columns for different stages of development. For example, we might have columns for "To Do", "In Progress", "Review", and "Done". Each issue should be assigned to a column.

Everyone should be able to see the progress of the robot code by looking at the project board.

The Software Lead(s) will keep the project board up to date.

Robot Code Challenges

This section contains a collection of challenges related to programming a robot using WPILib. These challenges DO NOT contain all of the needed information to complete them within their own pages. Nor do they necessarily only require the information in the previous sections. That's right! You'll need to use your problem-solving skills to figure out how to complete these challenges.

Challenge Structure (WIP)

Each challenge will be assigned on a thursday, and will be due for testing the following tuesday. These will be team-based challenges, usually with 2-3 people per team, although this depends on who is present on the day of the challenge.

You are required to create a GitHub repository forking off of each challenge's template, and submit a pull request to the template when you are done. We will then test your code on the robot, and give you feedback on how you did.

Although these challenges will not be "graded" in the traditional sense, the leads AND mentors will be keeping track of how well you do on these challenges, and will use this information to determine who will be in leadership roles both during the season, and in future years on the team. So it's important to take them seriously, and put in your best effort.

Challenge 1: Autonomous Differential Drive

Creating a robot program that can drive and turn set amounts with a differential drive robot.

Challenge Overview

This challenge is designed to test your abilities with simple motor control and feedback loops. Your task is to create a robot program that controls our differential test bot, and is capable of precisely driving set distances and turning set angles.

Completion Criteria

  • The robot uses command-based programming.
  • The robot has commands for driving a set distance and turning a set angle.
  • The robot has a low level of error in both driving and turning (lowest error "wins").

Conclusion

As you can see the criteria is very simple, but there are MANY things that could be done to improve the robot's usability and performance. This is where your creativity and problem-solving skills come in. Good luck!

Challenge 2

Challenge 3