Skip to content

Lab 15: Don't Block the Loop

Every earlier animated lab paced itself with sleep(), which freezes the entire program while it waits. This lab swaps sleep() for ticks_ms() so a face can blink on its own timer while the main loop stays free to do anything else — like check a button, which the very next lab adds.

Sample Program Code

The face blinks automatically every four seconds, without ever calling sleep():

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
# Lab 15: Don't Block the Loop
# Every earlier lab paced itself with sleep(), which freezes the whole
# program while it waits. This lab swaps sleep() for ticks_ms() so the
# face can blink on its own timer while the main loop stays free to do
# other things -- like check a button, which the next lab adds.
#
# There is a second kind of blocking on this display that the OLED kit
# never had to think about: a slow DRAW blocks just as hard as a sleep().
# display.fill(BLACK) takes real milliseconds because it is pushing
# 115,200 bytes, and nothing else in your program runs while it does. So
# the non-blocking pattern below is paired with the small-redraw pattern
# from lab 11. Both are needed; neither is enough alone.

import config
import shapes
from utime import ticks_ms, ticks_diff

display = config.init_display()
WHITE = config.WHITE
BLACK = config.BLACK
NO_FILL = config.NO_FILL
FILL = config.FILL

TOP_HALF = 3
BOTTOM_HALF = 12

HALF_WIDTH = config.WIDTH // 2
EYE_SPACING = 48
LEFT_EYE_X = HALF_WIDTH - EYE_SPACING
RIGHT_EYE_X = HALF_WIDTH + EYE_SPACING
EYE_Y = 100
EYE_RADIUS = 28
PUPIL_RADIUS = 10
BLINK_RADIUS_Y = 14
BLINK_Y = EYE_Y + 7
STROKE = 4
MOUTH_Y = 168
MOUTH_RADIUS_X = 48
MOUTH_RADIUS_Y = 24

EYE_BOX = EYE_RADIUS + 4

BLINK_EVERY_MS = 4000  # how often the face blinks on its own
BLINK_HOLD_MS = 150    # how long the eyes stay shut


def draw_open_eye(x):
    shapes.ellipse(display, x, EYE_Y, EYE_RADIUS, EYE_RADIUS, WHITE, FILL)
    shapes.ellipse(display, x, EYE_Y, PUPIL_RADIUS, PUPIL_RADIUS, BLACK, FILL)


def draw_closed_eye(x):
    for offset in range(STROKE):
        shapes.ellipse(display, x, BLINK_Y + offset, EYE_RADIUS,
                       BLINK_RADIUS_Y, WHITE, NO_FILL, TOP_HALF)


def draw_smile():
    for offset in range(STROKE):
        shapes.ellipse(display, HALF_WIDTH, MOUTH_Y - offset,
                       MOUTH_RADIUS_X, MOUTH_RADIUS_Y,
                       WHITE, NO_FILL, BOTTOM_HALF)


def set_eyes(blinking):
    for x in (LEFT_EYE_X, RIGHT_EYE_X):
        display.fill_rect(x - EYE_BOX, EYE_Y - EYE_BOX,
                          EYE_BOX * 2, EYE_BOX * 2, BLACK)
        if blinking:
            draw_closed_eye(x)
        else:
            draw_open_eye(x)


display.fill(BLACK)
draw_smile()
set_eyes(False)

blinking = False
last_blink = ticks_ms()
blink_started = 0

while True:
    now = ticks_ms()

    if not blinking and ticks_diff(now, last_blink) >= BLINK_EVERY_MS:
        blinking = True
        blink_started = now
        set_eyes(True)

    if blinking and ticks_diff(now, blink_started) >= BLINK_HOLD_MS:
        blinking = False
        last_blink = now
        set_eyes(False)

    # this loop never calls sleep(), so this spot is free for a button
    # check, a second animation, or anything else that needs to run often

# Things to try:
#
# 1. Replace set_eyes() with a version that does display.fill(BLACK) and
#    redraws the smile too. The loop still never sleeps -- but it is now
#    blocked for tens of milliseconds on every blink, which is the same
#    problem wearing a different hat. Time it and see.

Here's the resting face between blinks:

Simulated output of 15-no-blocking.py

A Second Kind of Blocking This Kit Has That the OLED Kit Didn't

The non-blocking pattern itself is identical to the OLED kit's — compare ticks_diff() against a remembered timestamp instead of sleeping through the wait. What's new is a second way to block that has nothing to do with sleep() at all: a slow draw blocks a loop exactly as hard as a sleep() call does, because display.fill(BLACK) takes real milliseconds to push 115,200 bytes, and nothing else in the program runs while it does.

So the non-blocking timer pattern here is always paired with the small-redraw pattern from Lab 11. Neither one alone is enough; a loop that never sleeps but still calls face.clear() on every blink is blocked in a different disguise.