Using the ChassisControllerBuilder

The ChassisControllerBuilder is a versatile and simple way to make a ChassisController. This tutorial will give an overview of how to use it and customize it for your robot.

Please read the preface on where to use builders: Where to use builders

Configuring motors

You must configure the motors. Skid-steer, x-drive, and h-drive configurations are supported.

Two motor skid-steer:

ChassisControllerBuilder()
    .withMotors(1, -2) // Left motor is 1, right motor is 2 (reversed)

Four motor skid-steer:

You may also use MotorGroups. You are not required to have an equal number of motors per side. There is no limit on the number of motors per side.

ChassisControllerBuilder()
    .withMotors(
        {-1, -2}, // Left motors are 1 & 2 (reversed)
        {3, 4}    // Right motors are 3 & 4
    )

X-Drive:

You may also use MotorGroups.

ChassisControllerBuilder()
    .withMotors(
        1,  // Top left
        -2, // Top right (reversed)
        -3, // Bottom right (reversed)
        4   // Bottom left
    )

H-Drive:

You may also use MotorGroups.

ChassisControllerBuilder()
    .withMotors(
        1,  // Left side
        -2, // Right side (reversed)
        3   // Middle
    )

Configuring your gearset and robot dimensions

You must configure the gearset and chassis dimensions to ensure that the gearsets in the motors are correct and to enable commanding the robot in real-life units (inches, degrees, etc.). If you specified non-integrated encoders using withSensors then withDimensions will refer to the wheels those encoders are attached to. Otherwise, withDimensions will refer to the driven wheels.

Gearset and dimensions:

ChassisControllerBuilder()
    // Green gearset, 4 inch wheel diameter, 11.5 inch wheelbase
    .withDimensions(AbstractMotor::gearset::green, {{4_in, 11.5_in}, imev5GreenTPR})

Gearset with an external ratio and dimensions:

ChassisControllerBuilder()
    // Green gearset, external ratio of (36.0 / 60.0), 4 inch wheel diameter, 11.5 inch wheelbase
    .withDimensions({AbstractMotor::gearset::green, (36.0 / 60.0)}, {{4_in, 11.5_in}, imev5GreenTPR})

Gearset and raw scales:

ChassisControllerBuilder()
    // Green gearset, straight scale of 1127.8696, turn scale of 2.875
    .withDimensions(AbstractMotor::gearset::green, {{1127.8696, 2.875}, imev5GreenTPR})

Configuring your sensors

If you do not use the motors' built-in encoders (e.g., you might use ADI encoders or rotation sensors), then you will need to pass those in as well. These sensors will not affect the ChassisControllerIntegrated controls because it uses the integrated control (and therefore, the encoders built-in to the motors).

Rotation Sensors

ChassisControllerBuilder()
    .withSensors(
        RotationSensor{1}, // Left encoder in V5 port 1
        RotationSensor{2, true}  // Right encoder in V5 port 2 (reversed)
    )

ADI Encoders

ChassisControllerBuilder()
    .withSensors(
        ADIEncoder{'A', 'B'}, // Left encoder in ADI ports A & B
        ADIEncoder{'C', 'D', true}  // Right encoder in ADI ports C & D (reversed)
    )

Configuring odometry

OkapiLib supports odometry for all chassis configurations. For more information about odometry, read the Odometry Tutorial.

Odometry using integrated encoders:

If you don't have external sensors, don't pass an extra ChassisScales to withOdometry.

ChassisControllerBuilder()
    .withMotors(1, -2) // Left motor is 1, right motor is 2 (reversed)
    // Green gearset, 4 inch wheel diameter, 11.5 inch wheelbase
    .withDimensions(AbstractMotor::gearset::green, {{4_in, 11.5_in}, imev5GreenTPR})
    .withOdometry() // Use the same scales as the chassis (above)
    .buildOdometry()

Odometry using external encoders:

If you have external sensors, you need to pass an extra ChassisScales to withOdometry to specify the dimensions for the tracking wheels. If you are using a ChassisControllerPID, these dimensions will be the same as the ones given to withDimensions, so you do not need to pass any dimensions to withOdometry. If you are using a ChassisControllerIntegrated, these dimensions will be different than the ones given to withDimensions.

ChassisControllerBuilder()
    .withMotors(1, -2) // Left motor is 1, right motor is 2 (reversed)
    // Green gearset, 4 inch wheel diameter, 11.5 inch wheelbase
    .withDimensions(AbstractMotor::gearset::green, {{4_in, 11.5_in}, imev5GreenTPR})
    .withSensors(
        ADIEncoder{'A', 'B'}, // Left encoder in ADI ports A & B
        ADIEncoder{'C', 'D', true}  // Right encoder in ADI ports C & D (reversed)
    )
    // Specify the tracking wheels diam (2.75 in), track (7 in), and TPR (360)
    .withOdometry({{2.75_in, 7_in}, quadEncoderTPR})
    .buildOdometry()

Configuring PID gains

If you want to use OkapiLib's PID control instead of the built-in control, you need to pass in two or three sets of PID gains.

Two sets:

ChassisControllerBuilder()
    .withGains(
        {0.001, 0, 0.0001}, // Distance controller gains
        {0.001, 0, 0.0001}  // Turn controller gains
    )

Three sets:

ChassisControllerBuilder()
    .withGains(
        {0.001, 0, 0.0001}, // Distance controller gains
        {0.001, 0, 0.0001}, // Turn controller gains
        {0.001, 0, 0.0001}  // Angle controller gains (helps drive straight)
    )

Configuring derivative filters

If you are using OkapiLib's PID control instead of the built-in control, you can pass in derivative term filters. These are applied to the PID controllers' derivative terms to smooth them. If you use OkapiLib's PID control but do not specify any derivative filters, the default filter is a PassthroughFilter.

One filter:

ChassisControllerBuilder()
    .withDerivativeFilters(
        std::make_unique<AverageFilter<3>>() // Distance controller filter
    )

Two filters:

ChassisControllerBuilder()
    .withDerivativeFilters(
        std::make_unique<AverageFilter<3>>(), // Distance controller filter
        std::make_unique<AverageFilter<3>>()  // Turn controller filter
    )

Three filters:

ChassisControllerBuilder()
    .withDerivativeFilters(
        std::make_unique<AverageFilter<3>>(), // Distance controller filter
        std::make_unique<AverageFilter<3>>(), // Turn controller filter
        std::make_unique<AverageFilter<3>>()  // Angle controller filter
    )

Configuring maximum velocity and voltage

You can change the default maximum velocity or voltage.

Max velocity:

The default max velocity depends on the gearset.

ChassisControllerBuilder()
    .withMaxVelocity(100)

Max voltage:

The default max voltage is 12000.

ChassisControllerBuilder()
    .withMaxVoltage(10000)

Configuring the controller settling behavior

You can change the SettledUtil that a ClosedLoopController gets when it is created by the builder, in order to change the settling behavior of the chassis.

ChassisControllerBuilder()
    .withClosedLoopControllerTimeUtil(50, 5, 250_ms)

Configuring the Logger

If you want the ChassisController and the classes it creates to log what they are doing, either for debugging or other purposes, you can supply a Logger. You can also enable logging globally.

Log to the PROS terminal:

ChassisControllerBuilder()
    .withLogger(
        std::make_shared<Logger>(
            TimeUtilFactory::createDefault().getTimer(), // It needs a Timer
            "/ser/sout", // Output to the PROS terminal
            Logger::LogLevel::debug // Most verbose log level
        )
    )

Log to the SD card:

ChassisControllerBuilder()
    .withLogger(
        std::make_shared<Logger>(
            TimeUtilFactory::createDefault().getTimer(), // It needs a Timer
            "/usd/test_logging.txt", // Output to a file on the SD card
            Logger::LogLevel::debug  // Most verbose log level
        )
    )