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:
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:
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:
- Grab the accelerometer’s X and Y axes
- Clamp the values to the range -1 to 1 (we don’t want things moving too fast!)
- Round the values to the nearest integer (so we stay still until the HAT is tilted quite a lot)
- Don’t bother yielding a movement unless one value is non-zero
- 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:
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:
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
.