The Problem: Odometry Drift

Dead-reckoning odometry (covered in the previous chapter) accumulates error every time the robot moves. After 20โ€“30 seconds of movement, the robot's internally-tracked position can be 1โ€“2 inches off from its real position.

In a match auton (15 seconds) this is usually acceptable. In a 60-second skills run targeting 75+ points, each scoring position must be accurate to within half an inch. A 1.5" positional error means the robot misses the goal entirely.

๐Ÿ”ด
Real consequence. GPS (another correction method) failed during the team's first skills run of the year. Distance sensors replaced it and have been reliable ever since. GPS accuracy is also limited to โ‰ˆ1โ€“2 cm, making it no more accurate than a well-tuned odometry system without correction.

The Core Idea

The VEX field walls are at a fixed, known distance from the field center. On a standard field, each wall is approximately 70.4375 inches from the origin in both the X and Y directions.

If we mount a distance sensor on the robot and point it at a wall, we can measure the distance from the sensor to the wall. Combined with the known wall position and the sensor's mounting offset from the robot's center, we can calculate the robot's exact position in that axis.

Field wall (at Y = +70.4375") โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ robot center distance sensor โ”‚ โ”‚ โ—‰โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถ distance โ”‚โ† sensorOffset โ†’โ”‚โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’โ”‚wallโ”‚ โ”‚ โ”‚ โ”‚โ†โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ robotY โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’โ”‚ robotY = wallY โˆ’ distance โˆ’ sensorOffset

In practice, the robot is never perfectly parallel to the wall, so we need to apply a cosine correction to account for the angle. More on that shortly.

Sensor Placement

This robot mounts two distance sensors:

SensorPortFacesMeasures
distanceBack Port 2 Toward the back wall Used to reset X position (tareXPos)
distanceRight Port 15 Toward the right wall Used to reset Y position (tareYPos) with a ฯ€/2 rotation offset
โ„น๏ธ
Why name one sensor "back" and use it for X? The sensor names reflect their physical mounting location, not which axis they correct. When the robot turns to face a particular wall, "distanceBack" might be facing left or right relative to the field. The axis being corrected depends on which direction the robot is facing at the moment of the reset, not the sensor's physical orientation. The sensorRotationOffset parameter handles this.

The Math

Simple case: robot perfectly facing the wall

Assume the robot is heading 0ยฐ (facing +Y / North) and we have a forward-facing distance sensor measuring distance to the North wall (at Y = +70.4375"):

robotY = wallY โˆ’ distance โˆ’ sensorOffset

Where:

Real case: robot at an angle to the wall

When the robot is not perfectly perpendicular to the wall, the distance sensor beam hits the wall at an angle. This makes the measured distance longer than the true perpendicular distance.

Wall โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ / โ† sensor beam (angled) / / โ† true perpendicular distance robot โ—‰โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ†’ wall โ”‚โ† heading offset (ฮธ) โ†’โ”‚ true_distance = measured_distance ร— cos(ฮธ)

The correction factor is |cos(ฮธ)| where ฮธ is the angle between the sensor beam and the wall normal. For a forward-facing sensor at heading ฮธ:

true_distance = measured_distance ร— |cos(heading)|

The sensor offset from the robot center must also be scaled the same way, because the sensor itself is displaced at the same angle:

robotY = wallY โˆ’ |cos(heading)| ร— measured_distance + |cos(heading)| ร— sensorOffset

And for a right-facing sensor (which measures along the X axis when heading is 90ยฐ), you add ฯ€/2 to the heading before taking the cosine:

robotX = wallX โˆ’ |cos(heading + ฯ€/2)| ร— measured_distance + |cos(heading + ฯ€/2)| ร— sensorOffset
โ„น๏ธ
Why |cos| and not just cos? Cosine can be negative (e.g., cos(180ยฐ) = โˆ’1). Taking the absolute value ensures we always get a positive scaling factor regardless of which direction the robot faces. The sign of the correction is handled by the wallInchesFromOrigin comparison instead.

The Implementation

The Chassis class in include/systems/chassis.hpp extends lemlib::Chassis with two new methods: tareYPos and tareXPos.

tareYPos โ€” Reset the Y coordinate

include/systems/chassis.hppvoid tareYPos(float wallInchesFromOrigin,   // โ‘ 
              pros::Distance *localizer,         // โ‘ก
              float sensorOffset,               // โ‘ข
              float scaleFactor = 1,            // โ‘ฃ
              float sensorRotationOffset = 0.0f) { // โ‘ค
  float robotY = wallInchesFromOrigin;

  // โ‘ฅ Take multiple readings and average them for accuracy
  std::vector<float> measuredDistance = {};
  int numberOfScans = 3;
  while (true) {
    numberOfScans--;
    measuredDistance.push_back(localizer->get_distance() * 0.0393701 * scaleFactor); // โ‘ฆ
    if (numberOfScans == 0) break;
    pros::delay(25);
  }

  // Average readings
  float sum = 0.0f;
  for (float d : measuredDistance) sum += d;
  float averageDistance = sum / measuredDistance.size();

  // โ‘ง Apply cosine correction and sensor offset
  if (wallInchesFromOrigin < 0) {          // South wall
    robotY = robotY
           + fabs(cos(getPose(true).theta + sensorRotationOffset)) * averageDistance
           - fabs(cos(getPose(true).theta + sensorRotationOffset)) * sensorOffset;
  } else {                                   // North wall
    robotY = robotY
           - fabs(cos(getPose(true).theta + sensorRotationOffset)) * averageDistance
           + fabs(cos(getPose(true).theta + sensorRotationOffset)) * sensorOffset;
  }

  // โ‘จ Apply the corrected Y, keep X and heading unchanged
  this->setPose(this->getPose().x, robotY, this->getPose().theta);
}
  • โ‘  wallInchesFromOrigin: The Y coordinate of the wall we're facing. Positive = North wall (+70.4375"), negative = South wall (โˆ’70.4375").
  • โ‘ก localizer: Pointer to the distance sensor facing that wall.
  • โ‘ข sensorOffset: Distance (inches) from the sensor to the robot's center. Negative means the sensor is behind center.
  • โ‘ฃ scaleFactor: A trim multiplier to correct for minor systematic sensor error found through testing.
  • โ‘ค sensorRotationOffset: Added to heading before cosine. Pass M_PI/2 when the sensor faces sideways (e.g., right-facing sensor measuring Y).
  • โ‘ฅ Three readings are taken 25 ms apart and averaged to reduce noise from vibrations or transient objects.
  • โ‘ฆ get_distance() returns millimetres. Multiply by 0.0393701 to convert to inches.
  • โ‘ง Cosine correction. For the South wall the robot's Y is further from it, so we add scaled distance. For the North wall we subtract. The sensor offset is applied with the same cosine factor.
  • โ‘จ Only Y changes. X and heading are untouched.

tareXPos โ€” Reset the X coordinate

Identical logic to tareYPos, but corrects X instead of Y. The cosine uses heading + ฯ€/2 (the sensor rotation is shifted by 90ยฐ relative to Y-axis sensors) and setPose updates X while preserving Y and heading.

include/systems/chassis.hpp// Key difference: the rotation offset is shifted by ฯ€/2
fabs(cos(getPose(true).theta + (sensorRotationOffset + std::numbers::pi / 2)))

// And we update X, not Y:
this->setPose(robotX, this->getPose().y, this->getPose().theta);

Using Distance Resets in Autons

Distance resets are inserted at natural waypoints in the auton where:

Call syntax used in skills

src/autons/autons.cpp// Reset X position using distanceBack (facing the West wall at โˆ’70.4375")
chassis.tareXPos(-70.4375, &distanceBack, -2.75, 1.00);

// Reset Y position using distanceRight (facing South wall at โˆ’70.4375")
// M_PI/2 offset because the sensor faces right (90ยฐ from forward)
chassis.tareYPos(-70.4375, &distanceRight, -4.75, 1.00, M_PI/2);
ParameterValueMeaning
wallInchesFromOriginโˆ’70.4375South or West field wall
localizer&distanceBackThe back-mounted distance sensor
sensorOffsetโˆ’2.75Sensor is 2.75" behind the robot center
scaleFactor1.00No scaling correction needed (tuned on field)
sensorRotationOffsetM_PI/2Right-facing sensor; add 90ยฐ before cosine

Placement strategy in the skills run

In the skills auton, resets happen whenever the robot turns to face a wall before moving to a scoring position. A typical sequence:

src/autons/autons.cpp  (skills)// After clearing the park zone, robot is near the corner.
// Turn to face the nearest walls and reset both axes.
chassis.turnToHeading(90, 10000, {.minSpeed = 20}, false);
chassis.tareXPos(-70.4375, &distanceBack,  -2.75, 1.00);        // โ† X reset
chassis.tareYPos(-70.4375, &distanceRight, -4.75, 1.00, M_PI/2); // โ† Y reset

// Now navigate to goal โ€” odometry is corrected, moves will be accurate.
chassis.turnToPoint(-1.65 * TILE_UNIT, -2 * TILE_UNIT, 10000);
chassis.moveToPoint(-1.65 * TILE_UNIT, -2 * TILE_UNIT, 10000);

Bonus: GPS as a Fallback Reset

Before distance sensors were used, the robot used a VEX GPS sensor for position correction. The GPS sensor reads field-tape-based visual landmarks to get an absolute position. The tareGps method in chassis.hpp implements this:

include/systems/chassis.hppvoid tareGps(pros::Gps *gps, bool heading = false) {
  pros::gps_position_s_t position = gps->get_position();
  double x = position.x * 39.37008; // meters โ†’ inches
  double y = position.y * 39.37008;
  double theta = gps->get_heading();

  // Optionally also reset the heading from GPS
  this->setPose(x, y, heading ? theta : this->getPose().theta);
}

GPS works well when the field tape is clean and the sensor has line-of-sight. However, it failed during an early skills run this season, which is why distance sensors are now the primary correction method.

Practical Tips

โœ…
Always stop before resetting. Even a slight vibration while the sensor is reading introduces noise. Call waitUntilDone() (or make the preceding move synchronous) before calling tareXPos / tareYPos.
โœ…
Test on the actual competition field. The exact distance of the walls from the field center varies slightly between fields (field setup tolerances). Measure with a tape measure on comp day and adjust wallInchesFromOrigin if needed. The loaderOffset variable in autons.cpp exists for exactly this reason.
โš ๏ธ
Clear field walls are inconsistent. The transparent acrylic field panels scatter the sensor's infrared beam unpredictably. For best results, only use distance resets when facing the wall head-on and the sensor is aimed at the aluminium base rail, not the clear panel.
โ„น๏ธ
Three readings are taken but you can increase this. The current implementation averages 3 readings. Increasing to 5 (as in tareXPos) reduces noise further at the cost of a small time penalty (~100 ms). For a skills run where accuracy matters more than time, use more readings.