Back

Activity 2.14: Sense and Decide

Motion & Autonomy — Day 4 of 4

divider

Activity 2.14

Sense and Decide

Key Concepts

The Control Loop

Tuning by Testing

Guarding a Sentinel

Nothing New Today

No new components. No new functions from the library. Every piece of this activity you have already built.

Today you join them — and that turns a machine that obeys into one that decides.

Three Steps, Forever

while True:
cm = read_distance() # SENSE
if cm < STOP_CM: # DECIDE
stop() # ACT
else:
drive(SPEED, SPEED)

Sense, decide, act. Round and round, many times a second. This shape runs thermostats, cruise control, elevators, and every robot you have ever seen.

Look at That Code Again

It has a bug. Not a typo — a reasoning bug, and it comes from something you built deliberately last session.

Terminal window
cm = -1 # nothing in range
-1 < 20 # True. Which is not what you meant.

-1 means nothing in range. An empty corridor. And -1 is less than 20, so the robot stops dead in front of wide open space.

Guard the Sentinel

if cm > 0 and cm < STOP_CM:

A sentinel value is only safe while every comparison remembers it exists. Yours is negative for exactly this reason — one extra test separates close from unknown.

Then There Are the Numbers

How close is too close? How long should it reverse? How far should it turn?

Nothing tells you. You pick something, run the robot, watch it fail, and change it — the same method you used to choose a threshold in 2.10, now with four numbers instead of one.

Today's Objectives

  • Combining a sensor reading and a motor command in one loop
  • Writing an obstacle-avoidance behavior
  • Guarding against an out-of-range reading
  • Tuning thresholds and timings by testing rather than guessing

Key Terms

Control Loop
Sense, decide, act, repeated indefinitely. The basic shape of every automatic machine.
Autonomous
Choosing what to do from its own sensor readings, with nobody steering.
Tuning
Adjusting numbers by testing, because no reference tells you the right values.
Guarding
Checking a value is real before comparing it, so a sentinel is not mistaken for a measurement.

'F' → Fullscreen

divider

Build

Everything stays wired from 2.12 and 2.13. Create a new MicroPython program named 2-14-rover.

Work on the floor, not the desk. The robot moves on its own from Task 1 onward.


Task 1: Sense and Stop

  • Bring your drive(), stop() and read_distance() functions into one script.
  • Drive forward while the way is clear, and stop when something is closer than STOP_CM.
  • Walk toward the robot and watch it stop.

Note the cm > 0 in the condition. Without it, an out-of-range reading of -1 counts as "too close" and the robot refuses to move in an empty room. Try removing it once, deliberately, so you have seen it happen.

[PHOTO PLACEHOLDER — Task 1: Rover Stopping in Front of an Obstacle]
Sense and Decide
from machine import Pin, PWM, time_pulse_us
import time
a_in1 = Pin(31, Pin.OUT)
a_in2 = Pin(32, Pin.OUT)
a_pwm = PWM(Pin(33))
a_pwm.freq(1000)
b_in1 = Pin(21, Pin.OUT)
b_in2 = Pin(35, Pin.OUT)
b_pwm = PWM(Pin(34))
b_pwm.freq(1000)
trigger = Pin(22, Pin.OUT)
echo = Pin(20, Pin.IN)
SPEED = 40000
STOP_CM = 20
TIMEOUT_US = 30000
def motor(in1, in2, pwm, speed):
if speed > 0:
in1.value(1)
in2.value(0)
pwm.duty_u16(speed)
elif speed < 0:
in1.value(0)
in2.value(1)
pwm.duty_u16(-speed)
else:
in1.value(0)
in2.value(0)
pwm.duty_u16(0)
def drive(left, right):
motor(b_in1, b_in2, b_pwm, left)
motor(a_in1, a_in2, a_pwm, right)
def stop():
drive(0, 0)
def read_distance():
trigger.value(0)
time.sleep_us(2)
trigger.value(1)
time.sleep_us(10)
trigger.value(0)
duration = time_pulse_us(echo, 1, TIMEOUT_US)
if duration < 0:
return -1
return duration // 58
while True:
cm = read_distance()
if cm > 0 and cm < STOP_CM:
stop()
else:
drive(SPEED, SPEED)
time.sleep(0.05)

Task 2: Back Away and Turn

  • Stopping is not enough — it just sits there. Write back_away() that reverses briefly, then spins.
  • Put the reverse and turn durations in named constants so they are easy to change.
  • Set the robot down in a corner and let it work its way out.
Sense and Decide
from machine import Pin, PWM, time_pulse_us
import time
a_in1 = Pin(31, Pin.OUT)
a_in2 = Pin(32, Pin.OUT)
a_pwm = PWM(Pin(33))
a_pwm.freq(1000)
b_in1 = Pin(21, Pin.OUT)
b_in2 = Pin(35, Pin.OUT)
b_pwm = PWM(Pin(34))
b_pwm.freq(1000)
trigger = Pin(22, Pin.OUT)
echo = Pin(20, Pin.IN)
SPEED = 40000
STOP_CM = 20
BACK_TIME = 0.4
TURN_TIME = 0.5
TIMEOUT_US = 30000
def motor(in1, in2, pwm, speed):
if speed > 0:
in1.value(1)
in2.value(0)
pwm.duty_u16(speed)
elif speed < 0:
in1.value(0)
in2.value(1)
pwm.duty_u16(-speed)
else:
in1.value(0)
in2.value(0)
pwm.duty_u16(0)
def drive(left, right):
motor(b_in1, b_in2, b_pwm, left)
motor(a_in1, a_in2, a_pwm, right)
def stop():
drive(0, 0)
def read_distance():
trigger.value(0)
time.sleep_us(2)
trigger.value(1)
time.sleep_us(10)
trigger.value(0)
duration = time_pulse_us(echo, 1, TIMEOUT_US)
if duration < 0:
return -1
return duration // 58
def back_away():
stop()
time.sleep(0.2)
drive(-SPEED, -SPEED)
time.sleep(BACK_TIME)
drive(SPEED, -SPEED)
time.sleep(TURN_TIME)
while True:
cm = read_distance()
if cm > 0 and cm < STOP_CM:
back_away()
else:
drive(SPEED, SPEED)
time.sleep(0.05)

Task 3: Start on a Button Press

  • A robot that drives the instant you plug it in is impossible to work with.
  • Wait for a button press before entering the loop.

The wait is a loop whose body does nothing at all — the same pass trick you used to wait for a reaction in 2.7.

Sense and Decide
from machine import Pin, PWM, time_pulse_us
import time
a_in1 = Pin(31, Pin.OUT)
a_in2 = Pin(32, Pin.OUT)
a_pwm = PWM(Pin(33))
a_pwm.freq(1000)
b_in1 = Pin(21, Pin.OUT)
b_in2 = Pin(35, Pin.OUT)
b_pwm = PWM(Pin(34))
b_pwm.freq(1000)
trigger = Pin(22, Pin.OUT)
echo = Pin(20, Pin.IN)
button = Pin(28, Pin.IN, Pin.PULL_DOWN)
SPEED = 40000
STOP_CM = 20
BACK_TIME = 0.4
TURN_TIME = 0.5
TIMEOUT_US = 30000
def motor(in1, in2, pwm, speed):
if speed > 0:
in1.value(1)
in2.value(0)
pwm.duty_u16(speed)
elif speed < 0:
in1.value(0)
in2.value(1)
pwm.duty_u16(-speed)
else:
in1.value(0)
in2.value(0)
pwm.duty_u16(0)
def drive(left, right):
motor(b_in1, b_in2, b_pwm, left)
motor(a_in1, a_in2, a_pwm, right)
def stop():
drive(0, 0)
def read_distance():
trigger.value(0)
time.sleep_us(2)
trigger.value(1)
time.sleep_us(10)
trigger.value(0)
duration = time_pulse_us(echo, 1, TIMEOUT_US)
if duration < 0:
return -1
return duration // 58
def back_away():
stop()
time.sleep(0.2)
drive(-SPEED, -SPEED)
time.sleep(BACK_TIME)
drive(SPEED, -SPEED)
time.sleep(TURN_TIME)
print("Press the button to start.")
while button.value() == 0:
pass
while True:
cm = read_distance()
if cm > 0 and cm < STOP_CM:
back_away()
else:
drive(SPEED, SPEED)
time.sleep(0.05)

Task 4: Say What It Is Thinking

  • Put the OLED back in and show the state and the current distance.
  • Translate -1 into something a human wants to read. Nobody should see the sentinel.
  • Now you can debug the robot while it drives, without a laptop.
Sense and Decide
from machine import Pin, PWM, time_pulse_us
import qwiic_large_oled
import time
a_in1 = Pin(31, Pin.OUT)
a_in2 = Pin(32, Pin.OUT)
a_pwm = PWM(Pin(33))
a_pwm.freq(1000)
b_in1 = Pin(21, Pin.OUT)
b_in2 = Pin(35, Pin.OUT)
b_pwm = PWM(Pin(34))
b_pwm.freq(1000)
trigger = Pin(22, Pin.OUT)
echo = Pin(20, Pin.IN)
button = Pin(28, Pin.IN, Pin.PULL_DOWN)
SPEED = 40000
STOP_CM = 20
BACK_TIME = 0.4
TURN_TIME = 0.5
TIMEOUT_US = 30000
oled = qwiic_large_oled.QwiicLargeOled()
oled.begin()
def motor(in1, in2, pwm, speed):
if speed > 0:
in1.value(1)
in2.value(0)
pwm.duty_u16(speed)
elif speed < 0:
in1.value(0)
in2.value(1)
pwm.duty_u16(-speed)
else:
in1.value(0)
in2.value(0)
pwm.duty_u16(0)
def drive(left, right):
motor(b_in1, b_in2, b_pwm, left)
motor(a_in1, a_in2, a_pwm, right)
def stop():
drive(0, 0)
def read_distance():
trigger.value(0)
time.sleep_us(2)
trigger.value(1)
time.sleep_us(10)
trigger.value(0)
duration = time_pulse_us(echo, 1, TIMEOUT_US)
if duration < 0:
return -1
return duration // 58
def back_away():
stop()
time.sleep(0.2)
drive(-SPEED, -SPEED)
time.sleep(BACK_TIME)
drive(SPEED, -SPEED)
time.sleep(TURN_TIME)
def show(oled, line1, line2):
oled.clear(oled.PAGE)
oled.print(line1)
oled.print(line2)
oled.display()
show(oled, "Rover ready", "press to start")
while button.value() == 0:
pass
while True:
cm = read_distance()
if cm < 0:
reading = "clear"
else:
reading = str(cm) + " cm"
if cm > 0 and cm < STOP_CM:
show(oled, "BLOCKED", reading)
back_away()
else:
show(oled, "Driving", reading)
drive(SPEED, SPEED)
time.sleep(0.05)
[PHOTO PLACEHOLDER — Task 4: Rover Driving With Its Status on Screen]

Task 5: Tune It

  • No new code. Run the robot at an obstacle repeatedly and change one constant at a time.
  • Find a STOP_CM that stops without bumping, and a TURN_TIME that reliably faces somewhere new.
  • Record what you tried and what happened. Changing two things at once teaches you nothing.

Challenge (Optional): Look Both Ways

  • Instead of always turning the same direction, back up, turn a little each way, measure, and go whichever is clearer.
  • This is the difference between escaping a corner by luck and escaping it on purpose.
divider

Checkpoint

  • The robot sits still until the button is pressed
  • It drives forward in open space and does not stop for nothing
  • It stops before touching an obstacle, then reverses and turns
  • Placed in a corner, it gets itself out within a few attempts
  • The screen shows what it is doing and never displays -1
divider

Reflection

Answer the following questions before submitting your work.

  1. Describe the sense–decide–act loop in your own words, naming which line of your program does each part. Why does it have to repeat rather than run once?
  2. Leaving out cm > 0 makes the robot refuse to move in an empty room. Explain how a value you invented on purpose in 2.13 became a bug in 2.14, and what that suggests about using special values.
  3. Nothing told you what STOP_CM or TURN_TIME should be. Describe how you found yours, and why changing one number at a time mattered.
divider

Submit

Submit the required files to the appropriate dropbox.

Activity Complete