3. Simple Demos

To get us warmed up before we attempt some complete applications, here’s some simple demos that use the functionality of the Sense HAT. Along with some demos there’s a small exercise, which you might like to try if you want to hone your skills with the library.

3.1. Rainbow Scroller

There are many different color systems, and the colorzero library that pisense relies upon implements several, including HSV (Hue, Saturation, Value). In this scheme, hue is essentially cyclic. This makes it quite easy to produce a scrolling rainbow display. We’ll construct an 8x8 array in which the hue of a color depends on the sum of its X and Y coordinates divided by 14 (as the maximum sum is 7 + 7), which will give us a nice range of hues. You can try this easily from the command line:

>>> from pisense import SenseHAT, array
>>> from colorzero import Color
>>> hat = SenseHAT()
>>> rainbow = array([
... Color(h=(x + y) / 14, s=1, v=1)
... for x in range(8)
... for y in range(8)
... ])
>>> hat.screen.array = rainbow

At this point you should have a nice rainbow on your display. How do we make this scroll? We simply construct a loop that increments the hue a tiny amount each time round. For example:

examples/rainbow.py
from __future__ import division  # for py2.x compatibility
from pisense import SenseHAT, array
from colorzero import Color
from time import sleep

hat = SenseHAT()
offset = 0.0
while True:
    rainbow = array([
        Color(h=(x + y) / 14 + offset, s=1, v=1)
        for x in range(8)
        for y in range(8)
    ])
    hat.screen.array = rainbow
    offset += 0.05
    sleep(0.05)

3.2. Joystick Movement

In this demo we’ll move a dot around the screen in response to joystick moves. The easiest way to interact with the joystick is to treat it as an iterator (treating it as if it’s a rather slow list that only provides another value when something happens to the joystick). Most of the time you’re not that interested in the joystick events themselves, but rather on what they mean to your application.

Hence our first step is to define a generator function that transforms joystick events into relative X, Y movements:

def movements(events):
    for event in events:
        if event.pressed:
            try:
                yield {
                    'left':  (-1, 0),
                    'right': (1, 0),
                    'up':    (0, -1),
                    'down':  (0, 1),
                }[event.direction]
            except KeyError:
                break  # enter exits

You can try this out from the command line like so:

>>> hat = SenseHAT()
>>> for x, y in movements(hat.stick):
...     print('x:', x, 'y:', y)
...
x: 1 y: 0
x: 1 y: 0
x: 0 y: 1
x: 0 y: 1
x: -1 y: 0

Note

You may see several control characters like ^[[C and ^[[D appearing as you play with this. These are the raw characters that represent the cursor keys; this output can be ignored. Press the joystick in (generate an “enter” event) when you want to terminate the loop.

Now, we’ll define another simple generator that transforms these into arrays for the display. Finally, we’ll use that output to drive the display:

examples/joystick_dot.py
from pisense import SenseHAT, array
from colorzero import Color

def movements(events):
    for event in events:
        if event.pressed:
            try:
                yield {
                    'left':  (-1, 0),
                    'right': (1, 0),
                    'up':    (0, -1),
                    'down':  (0, 1),
                }[event.direction]
            except KeyError:
                break  # enter exits

def arrays(moves):
    a = array(Color('black'))  # blank screen
    x = y = 3
    a[y, x] = Color('white')
    yield a  # initial position
    for dx, dy in moves:
        a[y, x] = Color('black')
        x = max(0, min(7, x + dx))
        y = max(0, min(7, y + dy))
        a[y, x] = Color('white')
        yield a
    a[y, x] = Color('black')
    yield a  # end with a blank display

with SenseHAT() as hat:
    for a in arrays(movements(hat.stick)):
        hat.screen.array = a

This pattern of programming, treating inputs as iterators and writing a series of transforms to produce screen arrays, will become a common theme in much of the rest of this manual.

Exercise

Can you convert the rainbow demo above to use an iterable for its display? Hint: the iterable doesn’t need to take any input because it’s not really transforming anything, just yielding outputs.

3.3. Orientation Sensing

Could we adapt the joystick example to “roll” the dot around the screen using the Inertial Measurement Unit (IMU)? Quite easily as it happens. The only thing that needs to change is the transformation that yields the changes in the X and Y positions. Instead of transforming joystick events, it needs to transform IMU readings.

As it happens, the IMU’s accelerometer is perfect for this task. When the HAT is tilted to the right, the X-axis of the accelerometer winds up pointing downward, which means it starts reading close to 1 (due to gravity). The same happens for the Y-axis when the HAT is tilted toward you. So, the transformation is quite trivial:

  1. Grab the accelerometer’s X and Y axes
  2. Clamp the values to the range -1 to 1 (we don’t want things moving too fast!)
  3. Round the values to the nearest integer (so we stay still until the HAT is tilted quite a lot)
  4. Don’t bother yielding a movement unless one value is non-zero
  5. Introduce a short delay (with sleep()) because the IMU is capable of spitting out readings hundreds of times a second, and we don’t want the dot shooting around that fast!

Here’s the modified movements function:

def movements(imu):
    for reading in imu:
        delta_x = int(round(max(-1, min(1, imu.accel.x))))
        delta_y = int(round(max(-1, min(1, imu.accel.y))))
        if delta_x != 0 or delta_y != 0:
            yield delta_x, delta_y
        sleep(1/10)

Again, you can try this function out from the command line in the same manner as the joystick; just pass the IMU component to it instead:

>>> from pisense import SenseHAT
>>> hat = SenseHAT()
>>> for x, y in movements(hat.imu):
...     print('x:', x, 'y:', y)
...
x: 1 y: 0
x: 1 y: 0
x: 0 y: 1
x: 0 y: 1
x: -1 y: 0

Here’s the whole thing put together. Note that the only substantial change from the joystick demo above is the movements function:

examples/imu_dot.py
from __future__ import division  # for py2.x compatibility
from pisense import SenseHAT, array
from colorzero import Color
from time import sleep

def movements(imu):
    for reading in imu:
        delta_x = int(round(max(-1, min(1, imu.accel.x))))
        delta_y = int(round(max(-1, min(1, imu.accel.y))))
        if delta_x != 0 or delta_y != 0:
            yield delta_x, delta_y
        sleep(1/10)

def arrays(moves):
    a = array(Color('black'))  # blank screen
    x = y = 3
    a[y, x] = Color('white')
    yield a  # initial position
    for dx, dy in moves:
        a[y, x] = Color('black')
        x = max(0, min(7, x + dx))
        y = max(0, min(7, y + dy))
        a[y, x] = Color('white')
        yield a
    a[y, x] = Color('black')
    yield a  # end with a blank display

with SenseHAT() as hat:
    for a in arrays(movements(hat.imu)):
        hat.screen.array = a

Exercise

Can you combine the orientation demo with the rainbow scroller and make the rainbow scroll in different directions based on the orientation of the board?

3.4. Environment Sensing

How about a simple thermometer? We’ll treat the thermometer as an iterator, and write a transform that produces a screen containing the temperature as both a number (in a small font), and a very basic chart which lights more elements as the temperature increases.

We’ll start with a function that takes a reading, limits it to the range of temperatures we’re interested in (0°C to 50°C), and distributes that evenly over the range 0 <= n < 64 (representing all 64 elements of the HAT’s display):

from __future__ import division  # for py2.x compatibility
from pisense import SenseHAT, array, draw_text, image_to_rgb
from colorzero import Color, Red
from time import sleep
import numpy as np

def thermometer(reading):
    t = max(0, min(50, reading.temperature)) / 50 * 64

Next, we need to construct the crude chart representing the temperature. For this we call array() and pass it a list of 64 Color objects which will be solid red if the element is definitely below the current temperature, a scaled red for the element at the current temperature, and black (off) if the element is above the current temperature. We also flip the result as we want the chart to start at the bottom and work its way up:

    screen = array([
        Color('red') if i < int(t) else
        Color('red') * Red(t - int(t)) if i < t else
        Color('black')
        for i in range(64)
    ])
    screen = np.flipud(screen)

Next, we call draw_text() which will return us a small Image object containing the rendered text (we’ve added some padding at the bottom so the text is “top aligned”). We’ll convert that to an array, and “add” that to the chart we’ve drawn (a simple method of overlaying) and then clip the result to the range 0 to 1 (because where the text overlays the chart we’ll probably exceed the bounds of the red channel):

    text = image_to_rgb(draw_text(str(int(round(reading.temperature))),
                                  'small.pil', foreground=Color('gray'),
                                  padding=(0, 0, 0, 3)))
    screen[:text.shape[0], :text.shape[1]] += text
    return screen.clip(0, 1)

Finally, here’s the whole thing put together:

examples/thermometer.py
from __future__ import division  # for py2.x compatibility
from pisense import SenseHAT, array, draw_text, image_to_rgb
from colorzero import Color, Red
from time import sleep
import numpy as np

def thermometer(reading):
    t = max(0, min(50, reading.temperature)) / 50 * 64
    screen = array([
        Color('red') if i < int(t) else
        Color('red') * Red(t - int(t)) if i < t else
        Color('black')
        for i in range(64)
    ])
    screen = np.flipud(screen)
    text = image_to_rgb(draw_text(str(int(round(reading.temperature))),
                                  'small.pil', foreground=Color('gray'),
                                  padding=(0, 0, 0, 3)))
    screen[:text.shape[0], :text.shape[1]] += text
    return screen.clip(0, 1)

with SenseHAT() as hat:
    for reading in hat.environ:
        hat.screen.array = thermometer(reading)
        sleep(0.5)

You can test this script by running it, then placing your finger on the humidity sensor (which is the sensor we’re using to read temperature). If the ambient temperature is below about 24°C you should see the reading rise quite quickly. Take your finger off the sensor and it should fall back down again.

Why, in this example, did we construct a function that took a single reading? Why did we not pass the environ iterator to the thermometer function? Quite simply because we didn’t have to: making an array for the screen works from a single reading. It doesn’t have any need to know prior readings, or to keep any state between frames, so it’s simplest to make it a straight-forward function. That said…

Exercise

Can you change the script to show whether the temperature is rising or falling? Hint: passing the iterator to the transform is one way to do this, but for a neater way (without passing the iterator), look up pairwise in itertools.