Skip to content
Ahmed Dabak edited this page Jul 24, 2026 · 1 revision

Eyes

Introduction

Eyes is the HC-SR04 ultrasonic distance sensor -- the two cylinders that look like eyes. It is a facade over the Ultrasonic module.

long cm = Eyes::distanceCm();

The sensor measures the distance to the nearest surface directly in front of Otto. It sees walls, hands, and furniture; it is poor at soft fabric, thin chair legs, and anything at a steep angle, because those scatter the echo away.

Reading the distance

void loop() {
  long cm = Eyes::distanceCm();
  Serial.println(cm);
  delay(100);
}

The value is in centimetres, already median filtered. Practical range for the HC-SR04 is roughly 3 cm to 200 cm; below about 3 cm the echo returns before the sensor is listening.

Asking questions instead

Most sketches do not want the number, they want a decision. These read as sentences and are the preferred form:

if (Eyes::closerThanCm(15))  Legs::walkBackward(2);
if (Eyes::fartherThanCm(50)) Legs::walkForward(1);

Each call takes its own fresh reading. If you need one reading used in several comparisons -- including for consistency -- read it once:

long cm = Eyes::distanceCm();
if (cm < 15)      Mouth::show(Icon::Surprised);
else if (cm < 40) Mouth::show(Icon::Confused);
else              Mouth::show(Icon::Happy);

Out of range

When nothing sends back an echo, the reading is Eyes::OUT_OF_RANGE_CM, which is 999.

long cm = Eyes::distanceCm();
if (cm >= Eyes::OUT_OF_RANGE_CM) {
  Serial.println(F("nothing in front"));
}

This matters for fartherThanCm(): an open room and a disconnected sensor both report "far away". If a wiring fault would be dangerous for your behaviour, compare against OUT_OF_RANGE_CM explicitly.

The median filter

Ultrasonic sensors produce occasional wild readings -- a stray echo, a reflection off the floor. A single spike can make a robot jump backwards for no reason.

distanceCm() therefore takes several readings and returns the median, which discards outliers without smearing real changes the way an average does. It is on by default with three samples:

OttoConfig cfg;
cfg.ultrasonic.medianFilter = true;   // default
cfg.ultrasonic.samples      = 5;      // 1..5, steadier but slower
OttoFlow::start(cfg);

Setting samples = 1, or medianFilter = false, gives you the raw single reading -- fastest, noisiest.

Timing

Every reading is a real physical measurement and takes real time. A ping waits up to config.ultrasonic.timeoutMicros (25 ms by default) when nothing answers, and the filter adds a 5 ms settling delay between samples.

With the defaults, a distanceCm() call costs a few milliseconds when something is nearby and up to about 85 ms when nothing is in range. That is the price of the filter -- turn samples down if your loop needs to be quicker.

Note Movement calls block too. While Legs::walkForward(2) is running, Otto is not looking at anything. Non-blocking motion is on the Roadmap; until then, alternate short movements with sensor checks.

Going lower: the Ultrasonic module

long raw   = Ultrasonic::readSingleCm();   // one unfiltered reading
unsigned long us = Ultrasonic::echoMicros(); // raw pulse width, 0 = timeout

echoMicros() is the honest bottom layer: the pulse width returned by pulseIn(). Centimetres are that value divided by 58. Use it when you want your own filtering, or when debugging a sensor you suspect is miswired -- a permanent 0 means no echo ever comes back.

Clone this wiki locally