Odometry

Odometry is the act of tracking a robot's absolute position. This information can be used by motion algorithms to drive to positions or turn to absolute angles.

An excellent overview of the odometry tracking algorithm can be found here.

Configuring Odometry

OkapiLib supports odometry for all chassis configurations. A chassis controller that uses odometry is configured using the ChassisControllerBuilder.

There are three general types of odometry supported by OkapiLib, though most feasible configurations are possible:

  • Two Integrated Encoder: Works out of the box with most chassis configurations and uses integrated motor encoders. Using the integrated encoders makes the odometry prone to wheel slip and inexact turning causing this odometry method to have limited accuracy. This method cannot track sideways movement.
  • Two External Encoder: Requires two external tracking wheels which track the ground. Minimizes wheel slip and often improves turning accuracy. This method cannot track sideways movement.
  • Three External Encoder: Requires three external tracking wheels to measure the robot's movement in all directions. Recommended for a holonomic chassis or any skid-steer chassis using omni wheels. This method will have the highest accuracy.

Odometry using integrated encoders:

If you are using integrated encoders, then the odometry scales are the same as your chassis scales. Don't pass an extra ChassisScales to withOdometry. Here is an example using ChassisControllerIntegrated:

ChassisControllerBuilder()
    .withMotors(1, -2) // left motor is 1, right motor is 2 (reversed)
    // green gearset, 4 inch wheel diameter, 11.5 inch wheel track
    .withDimensions(AbstractMotor::gearset::green, {{4_in, 11.5_in}, imev5GreenTPR})
    .withOdometry() // use the same scales as the chassis (above)
    .buildOdometry(); // build an odometry chassis

Here is the same example using ChassisControllerPID:

ChassisControllerBuilder()
    .withMotors(1, -2) // left motor is 1, right motor is 2 (reversed)
    .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)
    )
    // green gearset, 4 inch wheel diameter, 11.5 inch wheel track
    .withDimensions(AbstractMotor::gearset::green, {{4_in, 11.5_in}, imev5GreenTPR})
    .withOdometry() // use the same scales as the chassis (above)
    .buildOdometry(); // build an odometry chassis

Odometry using external encoders:

If you are using a ChassisControllerIntegrated, the chassis dimensions will be different than the odometry scales given to withDimensions. This is because ChassisControllerIntegrated still requires scales for the powered wheels, while the tracking wheels have a different set of scales. You will need to pass an extra ChassisScales to withOdometry to specify the scales for the tracking wheels.

ChassisControllerBuilder()
    .withMotors(1, -2) // left motor is 1, right motor is 2 (reversed)
    // green gearset, 4 inch wheel diameter, 11.5 inch wheel track
    .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 diameter (2.75 in), wheel track (7 in), and TPR (360)
    .withOdometry({{2.75_in, 7_in}, quadEncoderTPR})
    .buildOdometry();

If you are using a ChassisControllerPID with external sensors, the odometry scales will be the same as the ones given to withDimensions.

ChassisControllerBuilder()
    .withMotors(1, -2) // left motor is 1, right motor is 2 (reversed)
    .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)
    )
    .withSensors(
        ADIEncoder{'A', 'B'}, // left encoder in ADI ports A & B
        ADIEncoder{'C', 'D', true}  // right encoder in ADI ports C & D (reversed)
    )
    // green gearset, tracking wheel diameter (2.75 in), track (7 in), and TPR (360)
    .withDimensions(AbstractMotor::gearset::green, {{2.75_in, 7_in}, quadEncoderTPR})
    .withOdometry() // use the same scales as the chassis (above)
    .buildOdometry(); // build an odometry chassis

Odometry using three tracking wheels:

ThreeEncoderOdometry is enabled by providing a third sensor to withSensors. The scales then need to be expanded to incorporate the third sensor. Here is an example using ChassisControllerIntegrated:

ChassisControllerBuilder()
    .withMotors(1, -2) // left motor is 1, right motor is 2 (reversed)
    // green gearset, 4 inch wheel diameter, 11.5 inch wheel track
    .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)
        ADIEncoder{'E', 'F'}  // middle encoder in ADI ports E & F
    )
    // specify the tracking wheels diameter (2.75 in), track (7 in), and TPR (360)
    // specify the middle encoder distance (1 in) and diameter (2.75 in)
    .withOdometry({{2.75_in, 7_in, 1_in, 2.75_in}, quadEncoderTPR})
    .buildOdometry();

Here is the same example using ChassisControllerPID:

ChassisControllerBuilder()
    .withMotors(1, -2) // left motor is 1, right motor is 2 (reversed)
    .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)
    )
    .withSensors(
        ADIEncoder{'A', 'B'}, // left encoder in ADI ports A & B
        ADIEncoder{'C', 'D', true},  // right encoder in ADI ports C & D (reversed)
        ADIEncoder{'E', 'F'}  // middle encoder in ADI ports E & F
    )
    // green gearset, tracking wheel diameter (2.75 in), track (7 in), and TPR (360)
    // 1 inch middle encoder distance, and 2.75 inch middle wheel diameter
    .withDimensions(AbstractMotor::gearset::green, {{2.75_in, 7_in, 1_in, 2.75_in}, quadEncoderTPR})
    .withOdometry() // use the same scales as the chassis (above)
    .buildOdometry(); // build an odometry chassis

Using Odometry

The odometry controller built by buildOdometry is a DefaultOdomChassisController, which inherits from OdomChassisController. It supports all methods from ChassisController such as moveDistance or turnAngle, and adds odometry commands such as driveToPoint, turnToPoint, and turnToAngle.

Coordinates

OkapiLib supports two methods of representing a coordinate. which are defined in StateMode. The default is StateMode::FRAME_TRANSFORMATION, where positive x is forward and positive y is to the right. You can specify the StateMode to be used for odometry as a parameter to withOdometry.

To get and set the odometry state, use getState and setState.

Moving

OkapiLib moves by calculating necessary chassis movement given a desired state and an odometry state, and then forwarding those commands to a ChassisController. For example, typing

chassis->driveToPoint({1_ft, 1_ft}); // assume starting position of {0, 0, 0}

is equivalent to typing

chassis->turnAngle(45_deg);
chassis->moveDistance(1.4_ft); // sqrt(1^2 + 1^2)

The advantage of using these algorithms is that if the odometry state is slightly different than expected, OkapiLib will try to compensate so that the next position is accurate. Programming using odometry is much nicer, as the user can visualize the field and changing a movement will not affect the following movements.

Full Example:

Here is is a full example of odometry using ChassisControllerIntegrated and two tracking wheels:

std::shared_ptr<OdomChassisController> chassis =
  ChassisControllerBuilder()
    .withMotors(1, -2) // left motor is 1, right motor is 2 (reversed)
    // green gearset, 4 inch wheel diameter, 11.5 inch wheel track
    .withDimensions(AbstractMotor::gearset::green, {{4_in, 11.5_in}, imev5GreenTPR})
    // left encoder in ADI ports A & B, right encoder in ADI ports C & D (reversed)
    .withSensors(ADIEncoder{'A', 'B'}, ADIEncoder{'C', 'D', true})
    // specify the tracking wheels diameter (2.75 in), track (7 in), and TPR (360)
    .withOdometry({{2.75_in, 7_in}, quadEncoderTPR}, StateMode::FRAME_TRANSFORMATION)
    .buildOdometry();

// set the state to zero
chassis->setState({0_in, 0_in, 0_deg});
// turn 45 degrees and drive approximately 1.4 ft
chassis->driveToPoint({1_ft, 1_ft});
// turn approximately 45 degrees to end up at 90 degrees
chassis->turnToAngle(90_deg);
// turn approximately -90 degrees to face {5_ft, 0_ft} which is to the north of the robot
chassis->turnToPoint({5_ft, 0_ft});