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
| Quantity | Convention 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 |
| Heading | Clockwise 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
| Movement from field origin | Expected estimated change |
|---|---|
| 500 mm forward at 0 degrees | X = 0, Y = +500 mm |
| 500 mm forward at 90 degrees | X = +500, Y = 0 mm |
| 500 mm reverse at 90 degrees | X = -500, Y = 0 mm |
| Pure turn about reference | X/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
- Verify straight forward and reverse distances against floor marks.
- Check a controlled pure rotation about the selected reference.
- Test a measured quarter-circle and compare the endpoint.
- Run a simple out-and-back sequence from a known start.
- 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
| Situation | Effect |
|---|---|
| Sideways push or slip | Lateral motion is not observed |
| Tracker lifts from floor | Travel is missed |
| Wrong offset sign | Turns create false translation |
| Wrong scale | Distance error accumulates |
| Inertial drift | Travel projects into the wrong direction |
| Sensor reset while running | Baseline jump corrupts the estimate |
| Unmodelled reference-point lateral motion | One-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
- Transform: predict field changes for forward and reverse travel at 0, 90, 180 and 270 degrees.
- Correct: calculate raw and corrected travel for an offset pure turn.
- Trace: explain why the midpoint heading is used during an arc.
- Configure: document the scale, offset and starting pose.
- Verify: compare estimated and physical endpoints across three trials.
- Simulate: show a sideways displacement the sensors cannot observe.
- Fault: confirm invalid feedback does not leave a live-valid pose.
- 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
- VEX Rotation Sensor API
- VEX accumulated rotation and calibration API
- Purdue SIGBots arc-based odometry theory
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.