VEX V5 PYTHON | 06.3 ONE-WHEEL ODOMETRY

Guide objective

Estimate field X/Y from one forward tracking wheel and inertial rotation, with offset correction and a clearly stated no-sideways-motion assumption.

01 | THE SIMPLEST USEFUL POSITION MODEL

One forward tracking wheel measures travel along the robot’s forward/backward axis. An inertial sensor measures how its orientation changes. Combining them can estimate a tank-drive robot’s field position during ordinary rolling movement.

The missing measurement is sideways translation. This model assumes the selected robot reference point has negligible body-sideways movement.

That can be a useful approximation for a non-holonomic drivetrain rolling without lateral slip. It is unsuitable for measuring a sideways slide, defensive push or holonomic strafe.

02 | KEEP BODY AND FIELD FRAMES DISTINCT

The body axes turn; the field axes stay fixed
QuantityConvention used here
Body forward+Y relative to the robot
Body right+X relative to the robot
Field up+Y on the fixed field map
Field right+X on the fixed field map
HeadingClockwise from field-map up: 0 up, 90 right

At heading zero, forward travel increases field Y. At 90 degrees, the same forward travel increases field X. At 180 degrees, it decreases field Y.

These are declared mathematical conventions. An inertial sensor is not automatically aligned to field-map up. You supply a known initial field heading.

03 | START FROM A KNOWN POSE

A pose contains X, Y and heading for the chosen reference point. Place that point at a measured field location and record its physical orientation.

Capture the wheel and accumulated inertial rotation baselines at the same stationary start. There is no need to reset the sensor values: subsequent updates use differences.

The example maps the initial inertial rotation reading to START_HEADING_DEG, then adds measured rotation changes. It does not require the sensor’s displayed zero to match field zero.

Resetting an encoder or recalibrating the inertial sensor while tracking breaks this mapping. Stop the estimator and reinitialize from a known pose after such changes.

04 | CONVERT EACH WHEEL CHANGE

Use the calibrated combined scale from 06.2:

wheel_mm = (current_turns - previous_turns) * mm_per_sensor_turn

The scale already includes circumference and gearing. Do not multiply it by the gear ratio a second time.

Positive readings mean forward rolling, negative readings mean reverse. Preserve this sign through every update so reverse movement can reduce the estimated field coordinates appropriately.

05 | REMOVE ROTATION CAUSED BY THE OFFSET

A forward tracker mounted left or right of the selected reference rolls during a turn. Its raw travel includes a rotational component.

Using the signed left/right offset xF and clockwise rotation change θ in radians:

Forward arc travel at the reference = wheel travel + xF × θ.

A tracker 120 mm left has xF = -120 mm. During a clockwise 90-degree pure turn about the reference, it measures approximately +188.5 mm. Adding (-120 × π/2) removes that apparent forward travel.

Use the measured tracker-line offset, not drivetrain track width or the diagonal distance to the sensor housing.

06 | USE THE MIDPOINT OF THE TURN

During an interval containing both travel and turning, using only the ending heading points all movement in the final direction. Instead, use the heading halfway through the interval.

For constant-curvature movement, convert the forward arc into its chord using a factor of sin(θ/2)/(θ/2). The factor tends to 1 when θ tends to zero.

half = delta_theta / 2
factor = 1.0 if abs(half) < 1e-8 else math.sin(half) / half
chord = forward_arc * factor
mid_heading = previous_heading + half
field_dx = chord * math.sin(mid_heading)
field_dy = chord * math.cos(mid_heading)

Angles in trigonometric functions are radians. This interval model assumes negligible sideways motion and approximately constant curvature. Frequent updates reduce the impact of movement changing within an interval.

It is not a substitute for measuring a missing lateral component.

07 | WORK THROUGH STRAIGHT, REVERSE AND ARC MOVES

Signed distance and heading jointly determine field movement
Movement from field originExpected estimated change
500 mm forward at 0 degreesX = 0, Y = +500 mm
500 mm forward at 90 degreesX = +500, Y = 0 mm
500 mm reverse at 90 degreesX = -500, Y = 0 mm
Pure turn about referenceX/Y unchanged after offset correction

For a clockwise quarter-circle starting at heading zero with reference-path radius 400 mm, forward arc travel is 400 × π/2 ≈ 628.3 mm. The chord is approximately 565.7 mm at midpoint heading 45 degrees.

Its field displacement is therefore approximately +400 mm in X and +400 mm in Y. With xF = -120 mm, raw wheel travel is approximately 816.8 mm before offset correction.

08 | COMPLETE SENSOR-ONLY ESTIMATOR

Paste below # Begin project code with generated VEX imports and Brain setup above it. Use the configured ports, calibrated scale and signed offset for your actual robot. This diagnostic commands no motors.

# =============================================================================
# TEAM [MY TEAM NAME / NUMBER] | SENSOR-ONLY ONE-WHEEL ODOMETRY
# =============================================================================
# Keep generated VEX imports and brain above '# Begin project code'.
# Forward tracking Rotation Sensor: Smart Port 17, forward-positive.
# Inertial: Smart Port 20, verified clockwise-positive rotation.
# No motors or autonomous movement are commanded by this diagnostic.
# Do not duplicate these manual declarations in Devices.
import math
tracking = Rotation(Ports.PORT17, False)
inertial = Inertial(Ports.PORT20)

CONFIGURED = False
MM_PER_SENSOR_TURN = 219.44  # Example only; use your calibrated combined scale
FORWARD_X_MM = -120.0       # Tracking line left of chosen reference point
START_X_MM = 0.0
START_Y_MM = 0.0
START_HEADING_DEG = 0.0     # Field-map up = 0, right = 90
POLL_MS = 20
MAX_LOOP_GAP_MS = 100
DISPLAY_MS = 200

class OneWheelOdometry:
    def __init__(self, x_mm, y_mm, heading_deg, sensor_turns,
                 rotation_deg, mm_per_sensor_turn, forward_x_mm):
        self.x = x_mm
        self.y = y_mm
        self.theta = math.radians(heading_deg)
        self.previous_turns = sensor_turns
        self.previous_rotation = rotation_deg
        self.scale = mm_per_sensor_turn
        self.offset = forward_x_mm

    def update(self, sensor_turns, rotation_deg):
        if not all(math.isfinite(v) for v in (sensor_turns, rotation_deg)):
            raise ValueError("Invalid sensor reading")
        wheel_mm = (sensor_turns - self.previous_turns) * self.scale
        dtheta = math.radians(rotation_deg - self.previous_rotation)
        # Body +X right, +Y forward; theta positive clockwise.
        forward_arc = wheel_mm + self.offset * dtheta
        half = dtheta / 2.0
        # Constant-curvature interval: arc length to midpoint chord.
        factor = 1.0 if abs(half) < 1e-8 else math.sin(half) / half
        chord = forward_arc * factor
        mid_heading = self.theta + half
        dx = chord * math.sin(mid_heading)
        dy = chord * math.cos(mid_heading)
        self.x += dx
        self.y += dy
        self.theta += dtheta
        self.previous_turns = sensor_turns
        self.previous_rotation = rotation_deg
        return dx, dy

    def pose(self):
        return self.x, self.y, math.degrees(self.theta) % 360.0


# =============================================================================
# STARTUP: PHYSICAL START POSE + SENSOR BASELINES
# =============================================================================
if not CONFIGURED:
    print("Verify scale, offset, signs and physical start pose first.")
elif not inertial.installed():
    print("Inertial unavailable.")
else:
    # Stationary calibration may block. Keep still for baseline capture too.
    inertial.calibrate()
    first_turns = tracking.position(TURNS)
    first_rotation = inertial.rotation(DEGREES)
    if not all(math.isfinite(v) for v in
               (first_turns, first_rotation, START_X_MM, START_Y_MM,
                START_HEADING_DEG, MM_PER_SENSOR_TURN, FORWARD_X_MM)):
        raise ValueError("Invalid setup reading or configuration")
    if MM_PER_SENSOR_TURN <= 0:
        raise ValueError("Scale must be positive; set sensor direction separately")
    odom = OneWheelOdometry(START_X_MM, START_Y_MM, START_HEADING_DEG,
                           first_turns, first_rotation,
                           MM_PER_SENSOR_TURN, FORWARD_X_MM)
    previous_ms = brain.timer.time(MSEC)
    last_display_ms = previous_ms
    valid = True
    print("Estimate active; no sideways measurement available.")
    while valid:
        now = brain.timer.time(MSEC)
        if now - previous_ms > MAX_LOOP_GAP_MS:
            print("Timing fault: estimate invalid; reinitialize at known pose.")
            valid = False
            break
        previous_ms = now
        if not inertial.installed() or inertial.is_calibrating():
            print("Inertial unavailable: estimate invalid.")
            valid = False
            break
        try:
            turns = tracking.position(TURNS)
            rotation = inertial.rotation(DEGREES)
            odom.update(turns, rotation)
            if now - last_display_ms >= DISPLAY_MS:
                x, y, h = odom.pose()
                print("Estimated X %.1f mm, Y %.1f mm, heading %.1f deg" % (x, y, h))
                last_display_ms = now
        except Exception:
            print("Feedback fault: estimate invalid; do not use old pose as live.")
            valid = False
        wait(POLL_MS, MSEC)

The example updates at a nominal 20 ms and prints at 200 ms intervals. A long loop gap or feedback failure invalidates the estimate instead of presenting the previous pose as current.

The 100 ms gap threshold is a teaching policy, not an accuracy guarantee. Separate sensor calls are not an atomic sample. Test timing and measurement consistency before using the estimate in a movement controller.

Keep startup calibration stationary. It may block; the update-loop timing check does not supervise calibration.

09 | TEST AGAINST PHYSICAL LANDMARKS

  1. Verify straight forward and reverse distances against floor marks.
  2. Check a controlled pure rotation about the selected reference.
  3. Test a measured quarter-circle and compare the endpoint.
  4. Run a simple out-and-back sequence from a known start.
  5. Repeat three times under normal load and record physical position error.

Use an appropriate supported setup for manual diagnostic movement. Do not force a tank-drive robot sideways against its tyres.

In simulation, apply a sideways displacement with no wheel rotation or heading change. The estimate stays unchanged. This deliberately exposes the missing measurement.

10 | KNOW WHEN THE MODEL BREAKS

Report assumptions with the estimated pose
SituationEffect
Sideways push or slipLateral motion is not observed
Tracker lifts from floorTravel is missed
Wrong offset signTurns create false translation
Wrong scaleDistance error accumulates
Inertial driftTravel projects into the wrong direction
Sensor reset while runningBaseline jump corrupts the estimate
Unmodelled reference-point lateral motionOne-wheel assumption no longer fits

Even a rigid robot can have reference points with different sideways components during rotation. Select a meaningful reference near the normal drivetrain kinematic centre and test its behaviour.

One wheel plus inertial does not make lateral motion observable. Additional tracking wheels or independent position measurements are the next step when that limitation matters.

11 | INTEGRATE WITHOUT CONFLICTING OWNERS

The estimator reads sensors and maintains a pose; it does not drive the robot. A separate movement controller can use that pose once its validity and limitations are understood.

Use one owner for odometry updates and sensor baseline changes. In a larger project, readers should obtain a consistent pose snapshot rather than separate X/Y/heading values that can change between reads.

Define a validity flag and timestamp policy, and stop position-dependent routines when the estimate becomes invalid. Reinitialize only at a verified physical pose or through a deliberate measurement-correction strategy.

CHALLENGES | ESTIMATE AND EVALUATE

  1. Transform: predict field changes for forward and reverse travel at 0, 90, 180 and 270 degrees.
  2. Correct: calculate raw and corrected travel for an offset pure turn.
  3. Trace: explain why the midpoint heading is used during an arc.
  4. Configure: document the scale, offset and starting pose.
  5. Verify: compare estimated and physical endpoints across three trials.
  6. Simulate: show a sideways displacement the sensors cannot observe.
  7. Fault: confirm invalid feedback does not leave a live-valid pose.
  8. Evaluate: justify when this one-wheel model is sufficient and when another measurement is needed.
Success evidence

A coordinate diagram, signed calculations, saved estimator and physical validation log. State the no-sideways-motion assumption and distinguish simulated checks from measured trials.

OFFICIAL AND TECHNICAL REFERENCES

Coordinate conventions vary between references. The one-wheel implementation and worked examples are independently written learning material. VEX and VEXcode are trademarks of their respective owners.