File: python-usage.rst

package info (click to toggle)
luma.led-matrix 1.8.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 7,596 kB
  • sloc: python: 1,624; makefile: 157
file content (478 lines) | stat: -rw-r--r-- 17,614 bytes parent folder | download
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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
Python Usage
------------

8x8 LED Matrices
^^^^^^^^^^^^^^^^
For the matrix device, initialize the :py:class:`luma.led_matrix.device.max7219`
class, as follows:

.. code:: python

   from luma.core.interface.serial import spi, noop
   from luma.core.render import canvas
   from luma.led_matrix.device import max7219
   
   serial = spi(port=0, device=0, gpio=noop())
   device = max7219(serial)

The display device should now be configured for use. The specific
:py:class:`~luma.led_matrix.device.max7219` class  exposes a
:py:func:`~luma.led_matrix.device.max7219.display` method which takes an image
with attributes consistent with the capabilities of the configured device's
capabilities. However, for most cases, for drawing text and graphics primitives,
the canvas class should be used as follows:

.. code:: python

   from PIL import ImageFont

   font = ImageFont.truetype("examples/pixelmix.ttf", 8)
 
   with canvas(device) as draw:
       draw.rectangle(device.bounding_box, outline="white", fill="black")

The :py:class:`luma.core.render.canvas` class automatically creates an
:py:mod:`PIL.ImageDraw` object of the correct dimensions and bit depth suitable
for the device, so you may then call the usual Pillow methods to draw onto the
canvas.

As soon as the with scope is ended, the resultant image is automatically
flushed to the device's display memory and the :mod:`PIL.ImageDraw` object is
garbage collected.

.. note::
   The default Pillow font is too big for 8px high devices like the LED matrices
   here, so the `luma.examples <https://github.com/rm-hull/luma.examples>`_ repo
   inclues a small TTF pixel font called **pixelmix.ttf** (attribution: 
   http://www.dafont.com/) which just fits.

   Alternatively, a set of "legacy" fixed-width bitmap fonts are included in
   the `luma.core <https://github.com/rm-hull/luma.core>`__ codebase and may be
   used as follows:

   .. code:: python

     from luma.core.legacy import text
     from luma.core.legacy.font import proportional, CP437_FONT, LCD_FONT

     with canvas(device) as draw:
         text(draw, (0, 0), "A", fill="white", font=proportional(CP437_FONT))

   The fixed-width fonts can be "converted" on-the-fly to proportionally
   spaced by wrapping them with the :py:class:`luma.core.legacy.font.proportional` 
   class.

Scrolling / Virtual viewports
"""""""""""""""""""""""""""""
A single 8x8 LED matrix clearly hasn't got a lot of area for displaying useful
information. Obviously they can be daisy-chained together to provide a longer
line of text, but as this library extends `luma.core <https://github.com/rm-hull/luma.core>`_,
then we can use the :py:class:`luma.core.virtual.viewport` class to allow
scrolling support:

.. code:: python

   import time

   from luma.core.interface.serial import spi, noop
   from luma.core.render import canvas
   from luma.core.virtual import viewport
   from luma.led_matrix.device import max7219
   
   serial = spi(port=0, device=0, gpio=noop())
   device = max7219(serial)
   
   virtual = viewport(device, width=200, height=100)

   with canvas(virtual) as draw:
       draw.rectangle(device.bounding_box, outline="white", fill="black")
       draw.text((3, 3), "Hello world", fill="white")

   for offset in range(8):
       virtual.set_position((offset, offset))
       time.sleep(0.1)

Calling :py:meth:`~luma.core.virtual.viewport.set_position` on a virtual
viewport, causes the device to render what is visible at that specific
position; altering the position in a loop refreshes every time it is called,
and gives an animated scrolling effect.

By altering both the X and Y co-ordinates allows scrolling in any direction,
not just horizontally.

Color Model
"""""""""""
Any of the standard :mod:`PIL.ImageColor` color formats may be used, but since
the 8x8 LED Matrices are monochrome, only the HTML color names :py:const:`"black"` and
:py:const:`"white"` values should really be used; in fact, by default, any value
*other* than black is treated as white. The :py:class:`luma.core.render.canvas`
constructor does have a :py:attr:`dither` flag which if set to
:py:const:`True`, will convert color drawings to a dithered monochrome effect.

.. code:: python

  with canvas(device, dither=True) as draw:
      draw.rectangle(device.bounding_box, outline="white", fill="red")

Landscape / Portrait Orientation
""""""""""""""""""""""""""""""""
By default, cascaded matrices will be oriented in landscape mode. Should you
have an application that requires the display to be mounted in a portrait
aspect, then add a :py:attr:`rotate=N` parameter when creating the device:

.. code:: python

  from luma.core.interface.serial import spi, noop
  from luma.core.render import canvas
  from luma.led_matrix.device import max7219

  serial = spi(port=0, device=0, gpio=noop())
  device = max7219(serial, rotate=1) 

  # Box and text rendered in portrait mode
  with canvas(device) as draw:
      draw.rectangle(device.bounding_box, outline="white", fill="black")

*N* should be a value of 0, 1, 2 or 3 only, where 0 is no rotation, 1 is
rotate 90° clockwise, 2 is 180° rotation and 3 represents 270° rotation.

The :py:attr:`device.size`, :py:attr:`device.width` and :py:attr:`device.height`
properties reflect the rotated dimensions rather than the physical dimensions.

Daisy-chaining
""""""""""""""
The MAX7219 chipset supports a serial 16-bit register/data buffer which is
clocked in on pin DIN every time the clock edge falls, and clocked out on DOUT
16.5 clock cycles later. This allows multiple devices to be chained together.

If you have more than one device and they are daisy-chained together, you can
initialize the library in one of two ways, either using :py:attr:`cascaded=N` 
to indicate the number of daisychained devices:

.. code:: python

   from luma.core.interface.serial import spi, noop
   from luma.core.render import canvas
   from luma.led_matrix.device import max7219

   serial = spi(port=0, device=0, gpio=noop())
   device = max7219(serial, cascaded=3)

   with canvas(device) as draw:
      draw.rectangle(device.bounding_box, outline="white", fill="black")

Using :py:attr:`cascaded=N` implies there are N devices arranged linearly and
horizontally, running left to right.

Alternatively, the device configuration may configured with :py:attr:`width=W`
and :py:attr:`height=H`. These dimensions denote the number of LEDs in the all
the daisychained devices. The width and height *must* both be multiples of 8:
this has scope for arranging in blocks in, say 3x3 or 5x2 matrices (24x24 or
40x16 pixels, respectively).

Given 12 daisychained MAX7219's arranged in a 4x3 layout, the simple example
below,

.. code:: python

   from luma.core.interface.serial import spi, noop
   from luma.core.render import canvas
   from luma.core.legacy import text
   from luma.core.legacy.font import proportional, LCD_FONT
   from luma.led_matrix.device import max7219

   serial = spi(port=0, device=0, gpio=noop())
   device = max7219(serial, width=32, height=24, block_orientation=-90)

   with canvas(device) as draw:
      draw.rectangle(device.bounding_box, outline="white")
      text(draw, (2, 2), "Hello", fill="white", font=proportional(LCD_FONT))
      text(draw, (2, 10), "World", fill="white", font=proportional(LCD_FONT))

displays as:

.. image:: images/box_helloworld.jpg
   :alt: box helloworld


Trouble-shooting / common problems
""""""""""""""""""""""""""""""""""
Some online retailers are selling pre-assembled `'4-in-1' LED matrix displays
<http://www.ebay.co.uk/itm/371306583204>`_, but they appear to be wired 90°
out-of-phase such that horizontal scrolling appears as below:

.. image:: images/block_reorientation.gif
   :alt: block alignment

This can be rectified by initializing the :py:class:`~luma.led_matrix.device.max7219`
device with a parameter of :py:attr:`block_orientation=-90` (or +90, if your device is
aligned the other way):

.. code:: python

   from luma.core.interface.serial import spi, noop
   from luma.core.render import canvas
   from luma.led_matrix.device import max7219

   serial = spi(port=0, device=0, gpio=noop())
   device = max7219(serial, cascaded=4, block_orientation=-90)

Every time a display render is subsequenly requested, the underlying image
representation is corrected to reverse the 90° phase shift.

Similarly, in other pre-assembled configurations, the 4-in-1 blocks
arrange the 8x8 blocks in reverse order. In that case, you need to pass
a True value to parameter `blocks_arranged_in_reverse_order`, requesting
an additional pre-processing step that fixes this:

.. code:: python

   ...
   device = max7219(serial, cascaded=4, block_orientation=-90,
                    blocks_arranged_in_reverse_order=True)

7-Segment LED Displays
^^^^^^^^^^^^^^^^^^^^^^
For the 7-segment device, initialize the :py:class:`luma.core.virtual.sevensegment` 
class, and wrap it around a previously created :py:class:`~luma.led_matrix.device.max7219`
device:

.. code:: python
    
   from luma.core.interface.serial import spi, noop
   from luma.core.render import canvas
   from luma.core.virtual import sevensegment
   from luma.led_matrix.device import max7219

   serial = spi(port=0, device=0, gpio=noop())
   device = max7219(serial, cascaded=2)
   seg = sevensegment(device)

The **seg** instance now has a :py:attr:`~luma.core.virtual.sevensegment.text` 
property which may be assigned, and when it does will update all digits
according to the limited alphabet the 7-segment displays support. For example,
assuming there are 2 cascaded modules, we have 16 character available, and so
can write:

.. code:: python

   seg.text = "Hello world"

Rather than updating the whole display buffer, it is possible to update
'slices', as per the below example:

.. code:: python

   seg.text[0:5] = "Goodbye"

This replaces ``Hello`` in the previous example, replacing it with ``Gooobye``.
The usual python idioms for slicing (inserting / replacing / deleteing) can be
used here, but note if inserted text exceeds the underlying buffer size, a
:py:exc:`ValueError` is raised.

Floating point numbers (or text with '.') are handled slightly differently - the
decimal-place is fused in place on the character immediately preceding it. This
means that it is technically possible to get more characters displayed than the
buffer allows, but only because dots are folded into their host character

.. image:: images/IMG_2810.JPG
   :alt: max7219 sevensegment

WS2812 NeoPixels
^^^^^^^^^^^^^^^^
For a strip of neopixels, initialize the :py:class:`luma.led_matrix.device.ws2812`
class (also aliased to  :py:class:`luma.led_matrix.device.neopixel`), supplying a
parameter :py:attr:`cascaded=N` where *N* is the number of daisy-chained LEDs.

This script creates a drawing surface 100 pixels long, and lights up three specific 
pixels, and a contiguous block:

.. code:: python

   from luma.core.render import canvas
   from luma.led_matrix.device import ws2812
   
   device = ws2812(cascaded=100)

   with canvas(device) as draw:
       draw.point((0,0), fill="white")
       draw.point((4,0), fill="blue")
       draw.point((11,0), fill="orange")
       draw.rectange((20, 0, 40, 0), fill="red")

If you have a device like Pimoroni's `Unicorn pHat <https://shop.pimoroni.com/products/unicorn-phat>`_, 
initialize the device with :py:attr:`width=N` and :py:attr:`height=N` attributes instead:

.. code:: python

   from luma.core.render import canvas
   from luma.led_matrix.device import ws2812
   
   # Pimoroni's Unicorn pHat is 8x4 neopixels
   device = ws2812(width=8, height=4)

   with canvas(device) as draw:
       draw.line((0, 0, 0, device.height), fill="red")
       draw.line((1, 0, 1, device.height), fill="orange")
       draw.line((2, 0, 2, device.height), fill="yellow")
       draw.line((3, 0, 3, device.height), fill="green")
       draw.line((4, 0, 4, device.height), fill="blue")
       draw.line((5, 0, 5, device.height), fill="indigo")
       draw.line((6, 0, 6, device.height), fill="violet")
       draw.line((7, 0, 7, device.height), fill="white")

.. note::
   The ws2812 driver uses the `ws2812 <https://pypi.python.org/pypi/ws2812>`_
   PyPi package to interface to the daisychained LEDs. It uses DMA (direct memory
   access) via ``/dev/mem`` which means that it has to run in privileged mode
   (via ``sudo`` root access).

The same viewport, scroll support, portrait/landscape orientation and color model
idioms provided in luma.core are equally applicable to the ws2812 implementation.

Pimoroni Unicorn HAT
""""""""""""""""""""
Pimoroni sells the `Unicorn HAT <https://shop.pimoroni.com/products/unicorn-hat>`_, 
comprising 64 WS2812b NeoPixels in an 8x8 arrangement. The pixels are cascaded, but
arranged in a 'snake' layout, rather than a 'scan' layout. In order to accomodate this,
a translation mapping is required, as follows:

.. code:: python

    import time

    from luma.led_matrix.device import ws2812, UNICORN_HAT
    from luma.core.render import canvas

    device = ws2812(width=8, height=8, mapping=UNICORN_HAT)

    for y in range(device.height):
        for x in range(device.width):
            with canvas(device) as draw:
                draw.point((x, y), fill="green")
            time.sleep(0.5)

This should animate a green dot moving left-to-right down each line.

Pimoroni Unicorn HAT HD
"""""""""""""""""""""""
Pimoroni sells the `Unicorn HAT HD <https://shop.pimoroni.com/products/unicorn-hat-hd>`_,
comprising 256 high-intensity RGB LEDs in a 16x16 arrangement. The pixels are driven by an
ARM STM32F making the display appear as an SPI device:

.. code:: python

    import time

    from luma.led_matrix.device import unicornhathd
    from luma.core.interface.serial import spi, noop
    from luma.core.render import canvas

    serial = spi(port=0, device=0, gpio=noop())
    device = unicornhathd(serial)

    for y in range(device.height):
        for x in range(device.width):
            with canvas(device) as draw:
                draw.point((x, y), fill="green")
            time.sleep(0.5)

This should animate a green dot moving left-to-right down each line.

NeoSegments (WS2812)
""""""""""""""""""""
`@msurguy <https://twitter.com/msurguy?lang=en>`_ has `crowdsourced some WS2812 neopixels <https://www.crowdsupply.com/maksmakes/neosegment>`_ 
into a modular 3D-printed seven-segment unit. To program these devices:

.. code:: python

    import time

    from luma.led_matrix_device import neosegment

    neoseg = neosegment(width=6)
    
    # Defaults to "white" color initially
    neoseg.text = "NEOSEG"
    time.sleep(1)

    # Set the first char ('N') to red
    neoseg.color[0] = "red"
    time.sleep(1)

    # Set fourth and fifth chars ('S','E') accordingly
    neoseg.color[3:5] = ["cyan", "blue"]
    time.sleep(1)

    # Set the entire string to green
    neoseg.color = "green"

The :py:class:`~luma.led_matrix.device.neosegment` class extends :py:class:`~luma.core.virtual.sevensegment`,
so the same text assignment (Python slicing paradigms) can be used here as well -
see the earlier section for further details.

The underlying device is exposed as attribute :py:attr:`device`, so methods
such as :py:attr:`show`, :py:attr:`hide` and :py:attr:`contrast` are available.

Next-generation APA102 NeoPixels
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
APA102 RGB neopixels are easier to control that WS2812 devices - they are driven
using SPI rather than precise timings that the WS2812 devices need. Initialize the
:py:class:`luma.led_matrix.device.apa102` class, supplying a parameter
:py:attr:`cascaded=N` where *N* is the number of daisy-chained LEDs. 

The following script creates a drawing surface 8 pixels long, and lights up three 
specific pixels:

.. code:: python

   from luma.core.render import canvas
   from luma.led_matrix.device import apa102
   
   device = apa102(cascaded=8)

   with canvas(device) as draw:
       draw.point((0,0), fill="white")
       draw.point((0,1), fill="blue")
       draw.point((0,2), fill=(0xFF, 0x00, 0x00, 0x80))  # RGBA tuple, alpha controls brightness

APA102 RGB pixels can have their brightness individually controlled: by setting
the alpha chanel to a translucent value (as per the above example) will set the 
brightness accordingly.

Emulators
^^^^^^^^^
There are various `display emulators <http://github.com/rm-hull/luma.emulator>`_
available for running code against, for debugging and screen capture functionality:

* The :py:class:`luma.emulator.device.capture` device will persist a numbered
  PNG file to disk every time its :py:meth:`~luma.emulator.device.capture.display`
  method is called.

* The :py:class:`luma.emulator.device.gifanim` device will record every image
  when its :py:meth:`~luma.emulator.device.gifanim.display` method is called,
  and on program exit (or Ctrl-C), will assemble the images into an animated
  GIF.

* The :py:class:`luma.emulator.device.pygame` device uses the :py:mod:`pygame`
  library to render the displayed image to a pygame display surface. 

Invoke the demos with::

  $ python examples/clock.py -d capture --transform=led_matrix

or::

  $ python examples/clock.py -d pygame --transform=led_matrix
  
.. note::
   *Pygame* is required to use any of the emulated devices, but it is **NOT**
   installed as a dependency by default, and so must be manually installed
   before using any of these emulation devices (e.g. ``pip install pygame``).
   See the install instructions in `luma.emulator  <http://github.com/rm-hull/luma.emulator>`_
   for further details.


.. image:: images/emulator.gif
   :alt: max7219 emulator