Navigation and control

Navigation, control, and consistency

How a robot with no absolute position finished the same course the same way, run after run.

The robot moved by counting wheel rotations. Drive forward a set number of encoder counts, turn by spinning the wheels against each other, count again. It is the simplest way to navigate, and it works — for a while.

The problem is that it never self-corrects. A wheel that slips, a surface that grips differently, a turn that lands two degrees wide — each one is added to every measurement that follows. Two degrees at the compost drum becomes inches by the fertilizer lever. And a task missed by an inch scores exactly the same as a task not attempted.

So the question was never how to navigate accurately. It was how to stop being wrong from accumulating.

Everything it could sense

SensorNo.PinWhat it told the robot
Shaft encoders2P0_0, P3_1How far a wheel has turned, and by how much they differ
Bump switches2P3_7, P0_2Contact with a wall — the only absolute reference on board
CdS cell, red filter1P1_0The start light, and red-or-blue at the humidifier

Five devices. No camera, no rangefinder, no line-following sensors, and nothing at all that reports position. Everything below is built out of those five.

A filter instead of a colour sensor

One task turns on being able to tell red light from blue. A CdS cell cannot do that — it measures brightness, and nothing else.

Putting a red filter in front of it changes the question. Red light passes; blue light is absorbed. A colour the sensor cannot perceive becomes a brightness difference it can, and the whole decision collapses into a single threshold.

double colorValue = cdsCell.Value();

if (cdsCell.Value() < .55)   // red
{
    turn_left(50, 150);
}
else                          // blue
{
    turn_right(50, 150);
}
The entire colour decision. 0.55 was found by testing, not calculated.

The route, and what it knew

Read down the rail. Where it runs solid, the robot is in contact with something whose position is fixed and known, and every error it had accumulated is thrown away. Where it runs dashed, the robot is guessing — competently, but guessing — and the error is growing again.

  • Touching something known
  • Dead reckoning, error growing
  1. The start light

    Waiting in the starting area, against known walls, for the CdS cell to see red.

  2. To the compost drum

    Forward 2″, left 87°, forward 5″, right 34°, forward 5″.

  3. The compost drum

    Driven into the drum itself. Contact with the task is contact with something known.

  4. To the apple stump

    Back 4″, right 80°, forward 11.75″, arm down, left 85°, forward 9″.

  5. Square against the wall

    Reverse until both bump switches close. This is the fix added after qualifiers, where the bucket was missed.

  6. Up the ramp to the table

    Right 96°, forward 26″ — the longest blind run of the route.

  7. Square again

    Reverse into the wall a second time before placing the bucket.

  8. To the fertilizer levers

    Forward 3″, right 45°, forward 16.5″ — the distance depends on which lever the course names.

  9. The correct lever

    Flipped down, held five seconds, then lifted while the course confirms it.

  10. To the humidifier

    Square, forward 17.5″, then read the light and turn 150° one way or the other.

  11. The correct button

    Chosen by a single voltage threshold, then driven into.

  12. The window, then home

    Open and close the greenhouse window, back down the ramp, and press the final button.

The pattern is the whole strategy: never guess for long. Legs are kept short, and every one of them ends somewhere the robot can re-establish the truth.

Five ways of not accumulating error

Square against walls

Heading error accumulates with every turn, and nothing on board can measure heading.

Reverse into a wall until both bump switches close. One switch would only tell you that you touched something. Two, and the wall itself rotates the robot square before it can proceed — so the correction is to heading, not just position. The course's own geometry becomes the reference the robot doesn't carry.

while (!(bLeft.Value() && bRight.Value()))
{
    left_motor.SetPercent(-50);
    right_motor.SetPercent(-50);
}
left_motor.SetPercent(0);
right_motor.SetPercent(0);
Repeated at every checkpoint on the route.

Compensate for the battery

At Milestone 1 the robot travelled shorter distances as the day went on, running identical code.

A weaker battery means less power at the same commanded percentage, which means less distance. Rather than re-tune the numbers against a moving target, every movement scales its command by the measured voltage — so a command means the same thing at the start of a session and at the end of it.

percent = 11.5 / Battery.Voltage() * percent;
One line, in every movement function.

Count, don't time

Timed moves drift with surface, load, and charge.

Distance is expressed in encoder counts and the motors run until the average of both wheels reaches the target. Power is kept moderate on purpose: testing showed encoder accuracy fell away at high power, as momentum carried the robot past its own count.

int counts = inches * countsPerInch;

right_encoder.ResetCounts();
left_encoder.ResetCounts();

right_motor.SetPercent(percent);
left_motor.SetPercent(percent);

while ((left_encoder.Counts() + right_encoder.Counts()) / 2.0 < counts);
move_straight(), the most-called function in the program.

Retry, but bounded

A task can fail silently, and a robot that keeps trying forever scores nothing at all.

The course can be asked whether a lever is still flipped. The robot lifts the arm in small increments while easing backwards, re-checking each time — and gives up after fifteen attempts. The cap is the important part: a failure costs a couple of seconds instead of the whole run.

int v = 1;
while (v <= 15 && RCS.isLeverFlipped())
{
    Sleep(.05);
    lever_arm.SetDegree(5 * v);
    move_straight(-50, .25);
    v++;
}
The only place the robot asks the course whether it succeeded.

Nudge instead of shove

One decisive push failed. Driving hard at the window did not open it, and a single sweep of the arm did not place the bucket.

Both became a sequence of small movements instead. The window opened on repeated short pushes; the bucket was lowered in stages, the arm dropping twenty degrees at a time while the robot eased back. Small increments fail small — and each one can be corrected before the next.

for (int i = 1; i < 6; i++)
{
    lever_arm.SetDegree(170 - i * 20);
    move_straight(-25, 0.75);
    Sleep(0.5);
}
Placing the apple bucket on the high table.

One idea, five times

Square against a wall, scale power by battery voltage, count instead of time, cap every retry, move in increments. They look like five separate tricks, and they are all the same instinct applied in different places:

When you cannot know where you are, keep returning to places where you do — and never let a mistake get large enough to matter.

The code above is quoted from the team’s own listing, written five days before the showcase. It is a working snapshot rather than the final program: the window and end-button routines had not been added yet, and fertLever() still has the lever number hard-coded for testing where the finished version asks the course. The patterns shown are the ones that survived to the run.