├── .coveragerc ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ ├── main.yml │ └── publish-to-pypi.yml ├── .gitignore ├── .readthedocs.yml ├── CHANGES.rst ├── CONTRIBUTING.rst ├── LICENSE.rst ├── MANIFEST.in ├── README.rst ├── doc ├── .gitignore ├── Makefile ├── api-documentation.rst ├── conf.py ├── fritzing │ └── 3x 7segment.fzz ├── images │ ├── 7-segment.svg │ ├── 7segment.jpg │ ├── BL-M12A881.png │ ├── IMG_2810.JPG │ ├── block_reorientation.gif │ ├── box_helloworld.jpg │ ├── devices.jpg │ ├── emulator.gif │ ├── level-shifter.jpg │ ├── matrix.jpg │ ├── matrix_cascaded.jpg │ └── raspi-spi.png ├── index.rst ├── install.rst ├── intro.rst ├── notes.rst ├── python-usage.rst ├── references.rst └── tech-spec │ ├── APA102.pdf │ ├── MAX7219.pdf │ ├── TM1637.pdf │ ├── WS2812.pdf │ └── WS2812B.pdf ├── examples ├── apa102_demo.py ├── box_demo.py ├── issue_108.py ├── larson_hue.py ├── matrix_demo.py ├── neopixel_crawl.py ├── neopixel_demo.py ├── neosegment_demo.py ├── sevensegment_demo.py ├── silly_clock.py └── view_message.py ├── luma └── led_matrix │ ├── __init__.py │ ├── const.py │ ├── device.py │ └── segment_mapper.py ├── pyproject.toml ├── pytest.ini ├── setup.cfg ├── setup.py ├── tests ├── baseline_data.py ├── helpers.py ├── reference │ ├── data │ │ ├── demo_unicornhathd.json │ │ └── demo_unicornhathd_alphablend.json │ └── images │ │ └── neosegment.png ├── test_apa102.py ├── test_max7219.py ├── test_neosegment.py ├── test_segment_mapper.py ├── test_unicornhathd.py └── test_ws2812.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | source = luma.led_matrix 4 | omit = 5 | luma/__init__.py 6 | setup.py 7 | .tox/* 8 | doc/* 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > Firstly thanks for taking the time to file an issue. Chances are, you are not the first (and 2 | > wont be the last) to have this problem: By creating a public issue, others can browse and 3 | > solve their issues - possibly in a self-service style - just by reading the discourse on your 4 | > ticket, so try and be clear in the describing the problem you have. 5 | > 6 | > Secondly, github issues are not _just_ for raising problems - if you have a question, a 7 | > documentation fix, or suggested improvement, please get in touch. 8 | > 9 | > Lastly, there are a number of related LUMA projects - please check to make sure to create 10 | > the issue in the right GitHub repository. 11 | 12 | #### Type of Raspberry Pi 13 | > Not all Pi's are equal at the hardware level - what works on one, might not work on the next. 14 | > This library has been tested on every variant except the RPi3. 15 | 16 | 17 | 18 | #### Linux Kernel version 19 | > Paste in the output from running `uname -a` at the command line on your Pi. 20 | 21 | 22 | 23 | #### Expected behaviour 24 | > Add a few concise notes about what you are expecting to happen. 25 | > Even better, if you paste in a code sample that demonstrates what you want to achieve. 26 | 27 | 28 | 29 | #### Actual behaviour 30 | > Now add some details about what actually happened - if there is an unexpected crash, paste in the 31 | > [traceback info](https://www.google.com/search?q=python+traceback#q=What+is+a+traceback%3F). 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: luma.led_matrix 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | python-minor-version: [8, 9, 10, 11, 12, 13] 16 | name: Python 3.${{ matrix.python-minor-version }} 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Setup pip cache 20 | uses: actions/cache@v4 21 | id: pipcache 22 | with: 23 | path: ~/.cache/pip 24 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 25 | restore-keys: | 26 | ${{ runner.os }}-py3${{ matrix.python-minor-version }}-pip- 27 | - name: Install system dependencies 28 | run: sudo apt-get install graphviz 29 | - name: Set up Python 30 | uses: actions/setup-python@v5 31 | with: 32 | python-version: 3.${{ matrix.python-minor-version }} 33 | check-latest: true 34 | - name: Display Python version 35 | run: python -c "import sys; print(sys.version)" 36 | - name: Install Python packages 37 | run: pip install --upgrade setuptools pip wheel tox coveralls 38 | - name: Run tests 39 | env: 40 | TOX_ENV: py3${{ matrix.python-minor-version }} 41 | run: | 42 | python_env=$(echo $TOX_ENV | sed -e s/-dev$//) 43 | python -m tox -e ${python_env} 44 | - name: QA 45 | env: 46 | TOX_ENV: qa,doc 47 | run: python -m tox -e $TOX_ENV 48 | - name: Upload Coverage 49 | run: coveralls --service=github 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | COVERALLS_FLAG_NAME: ${{ matrix.test-name }} 53 | COVERALLS_PARALLEL: true 54 | 55 | coveralls: 56 | name: Coveralls 57 | needs: build 58 | runs-on: ubuntu-latest 59 | container: python:3-slim 60 | steps: 61 | - name: Finished 62 | run: | 63 | pip3 install --upgrade coveralls 64 | coveralls --finish 65 | env: 66 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 67 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | permissions: 4 | id-token: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - '[0-9]+.[0-9]+.[0-9]+' 10 | 11 | jobs: 12 | build-and-publish: 13 | name: Build and publish Python package 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Set up Python 3.9 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: 3.9 21 | - name: Install pypa/build 22 | run: | 23 | python -m pip install --upgrade setuptools pip wheel twine 24 | python -m pip install build --user 25 | - name: Build a binary wheel and a source tarball 26 | run: | 27 | python -m build --sdist --wheel --outdir dist/ . 28 | twine check --strict dist/* 29 | - name: Publish package to test PyPI 30 | if: startsWith(github.ref, 'refs/tags') 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | user: __token__ 34 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 35 | repository_url: https://test.pypi.org/legacy/ 36 | - name: Publish package to PyPI 37 | if: startsWith(github.ref, 'refs/tags') 38 | uses: pypa/gh-action-pypi-publish@release/v1 39 | with: 40 | user: __token__ 41 | password: ${{ secrets.PYPI_API_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | MANIFEST 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | .pytest_cache/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | doc/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | .ropeproject 60 | *~ 61 | 62 | # OSX 63 | .DS_Store 64 | 65 | # IDE 66 | .project 67 | .pydevproject 68 | .settings/ 69 | .vscode 70 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | apt_packages: 14 | - graphviz 15 | 16 | # Build documentation in the doc/ directory with Sphinx 17 | sphinx: 18 | configuration: doc/conf.py 19 | 20 | # Install dependencies 21 | python: 22 | install: 23 | - method: pip 24 | path: . 25 | extra_requirements: 26 | - docs 27 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | ChangeLog 2 | --------- 3 | 4 | +------------+------------------------------------------------------------------------+------------+ 5 | | Version | Description | Date | 6 | +============+========================================================================+============+ 7 | | **1.8.0** | * Drop support for Python 3.7 | 2024/11/02 | 8 | | | * Improve performance of APA102 driver | | 9 | +------------+------------------------------------------------------------------------+------------+ 10 | | **1.7.1** | * Documentation fixes | 2023/10/05 | 11 | +------------+------------------------------------------------------------------------+------------+ 12 | | **1.7.0** | * Drop support for Python 3.6 | 2022/10/19 | 13 | | | * Switch to implicit namespace package configuration | | 14 | | | * luma.core 2.4.0 or newer is required now | | 15 | +------------+------------------------------------------------------------------------+------------+ 16 | | **1.6.1** | * Trigger publish on github actions | 2022/01/09 | 17 | +------------+------------------------------------------------------------------------+------------+ 18 | | **1.6.0** | * Remove redundant ``ws2812`` package | 2022/01/03 | 19 | +------------+------------------------------------------------------------------------+------------+ 20 | | **1.5.0** | * Drop support for Python 2.7, only 3.5 or newer is supported now | 2020/07/04 | 21 | +------------+------------------------------------------------------------------------+------------+ 22 | | **1.4.1** | * Make ``contrast`` an optional constructor argument | 2019/12/08 | 23 | +------------+------------------------------------------------------------------------+------------+ 24 | | **1.4.0** | * Rework namespace handling for luma sub-projects | 2019/06/16 | 25 | +------------+------------------------------------------------------------------------+------------+ 26 | | **1.3.1** | * Fix alpha-channel blending for Unicorn Hat HD display | 2019/05/26 | 27 | +------------+------------------------------------------------------------------------+------------+ 28 | | **1.3.0** | * Add support for Pimoroni's Unicorn Hat HD | 2019/05/26 | 29 | +------------+------------------------------------------------------------------------+------------+ 30 | | **1.2.0** | * Add option to control if 8x8 blocks are arranged in reverse order | 2019/04/20 | 31 | | | * Add (approximations of) more characters for 7-segment displa | | 32 | | | * Documentation updates | | 33 | +------------+------------------------------------------------------------------------+------------+ 34 | | **1.1.1** | * Fix unicode warning | 2018/09/26 | 35 | +------------+------------------------------------------------------------------------+------------+ 36 | | **1.1.0** | * Add degree symbol to segment mapper charmap | 2018/09/18 | 37 | +------------+------------------------------------------------------------------------+------------+ 38 | | **1.0.8** | * Use DMA channel 10 (rather than ch. 5) for WS2812 NeoPixels | 2018/01/23 | 39 | +------------+------------------------------------------------------------------------+------------+ 40 | | **1.0.7** | * Use ``extras_require`` in ``setup.py`` for ARM dependencies | 2017/11/26 | 41 | +------------+------------------------------------------------------------------------+------------+ 42 | | **1.0.6** | * Version number available as ``luma.led_matrix.__version__`` now | 2017/11/23 | 43 | +------------+------------------------------------------------------------------------+------------+ 44 | | **1.0.5** | * Conditionally install WS2812 packages if Linux/ARM7L only | 2017/10/22 | 45 | +------------+------------------------------------------------------------------------+------------+ 46 | | **1.0.4** | * Make wheel universal | 2017/10/22 | 47 | | | * Minor documentation fixes | | 48 | +------------+------------------------------------------------------------------------+------------+ 49 | | **1.0.3** | * Explicitly state 'UTF-8' encoding in setup when reading files | 2017/10/18 | 50 | +------------+------------------------------------------------------------------------+------------+ 51 | | **1.0.2** | * Setup fails due to programmer not understanding basic Python ... | 2017/08/05 | 52 | +------------+------------------------------------------------------------------------+------------+ 53 | | **1.0.1** | * Setup on Python 3 fails due to hyphen in package name | 2017/08/05 | 54 | +------------+------------------------------------------------------------------------+------------+ 55 | | **1.0.0** | * Stable release (remove all deprecated methods & parameters) | 2017/07/30 | 56 | +------------+------------------------------------------------------------------------+------------+ 57 | | **0.11.1** | * Add Python3 compatibility for neopixels/neosegments | 2017/07/29 | 58 | +------------+------------------------------------------------------------------------+------------+ 59 | | **0.11.0** | * Alternative WS2812 low level implementation | 2017/07/21 | 60 | | | * Add support for @msurguy's modular NeoSegments | | 61 | +------------+------------------------------------------------------------------------+------------+ 62 | | **0.10.1** | * Add block_orientation=180 option | 2017/05/01 | 63 | +------------+------------------------------------------------------------------------+------------+ 64 | | **0.10.0** | * **BREAKING CHANGE:** Move sevensegment class to | 2017/04/22 | 65 | | | ``luma.core.virtual`` package | | 66 | +------------+------------------------------------------------------------------------+------------+ 67 | | **0.9.0** | * Add support for APA102 RGB neopixels | 2017/03/30 | 68 | +------------+------------------------------------------------------------------------+------------+ 69 | | **0.8.0** | * Change MAX7219's block_orientation to support ±90° angle correction | 2017/03/19 | 70 | | | * Deprecate "vertical" and "horizontal" block_orientation | | 71 | +------------+------------------------------------------------------------------------+------------+ 72 | | **0.7.0** | * **BREAKING CHANGE:** Move sevensegment class to | 2017/03/04 | 73 | | | ``luma.led_matrix.virtual`` package | | 74 | | | * Documentation updates & corrections | | 75 | +------------+------------------------------------------------------------------------+------------+ 76 | | **0.6.2** | * Allow MAX7219 and NeoPixel driver constructors to accept any args | 2017/03/02 | 77 | +------------+------------------------------------------------------------------------+------------+ 78 | | **0.6.1** | * Restrict exported Python symbols from ``luma.led_matrix.device`` | 2017/03/02 | 79 | +------------+------------------------------------------------------------------------+------------+ 80 | | **0.6.0** | * Add support for arbitrary MxN matrices rather than a single chain | 2017/02/22 | 81 | +------------+------------------------------------------------------------------------+------------+ 82 | | **0.5.3** | * Huge performance improvements for cascaded MAX7219 devices | 2017/02/21 | 83 | | | * Documentation updates | | 84 | +------------+------------------------------------------------------------------------+------------+ 85 | | **0.5.2** | * Add apostrophe representation to seven-segment display | 2017/02/19 | 86 | | | * Deprecate ``luma.led_matrix.legacy`` (moved to ``luma.core.legacy``) | | 87 | +------------+------------------------------------------------------------------------+------------+ 88 | | **0.4.4** | * Support both common-row anode and common-row cathode LED matrices | 2017/02/02 | 89 | +------------+------------------------------------------------------------------------+------------+ 90 | | **0.4.3** | * Add translation mapping to accomodate Pimoroni's 8x8 Unicorn HAT | 2017/01/29 | 91 | | | * MAX7219 optimizations | | 92 | +------------+------------------------------------------------------------------------+------------+ 93 | | **0.4.2** | * Fix bug in neopixel initialization | 2017/01/27 | 94 | | | * Improved demo scripts | | 95 | | | * Additional tests | | 96 | +------------+------------------------------------------------------------------------+------------+ 97 | | **0.4.0** | * Add support for WS2812 NeoPixel strips/arrays | 2017/01/23 | 98 | +------------+------------------------------------------------------------------------+------------+ 99 | | **0.3.3** | * Fix for dot muncher: not handling full-stop at line end | 2017/01/21 | 100 | | | * Documentation updates | | 101 | +------------+------------------------------------------------------------------------+------------+ 102 | | **0.3.2** | * Replace bytearray with ``mutable_string`` implementation | 2017/01/20 | 103 | | | * More tests | | 104 | +------------+------------------------------------------------------------------------+------------+ 105 | | **0.3.1** | * Python 3 compatibility (fix exception in bytearray creation) | 2017/01/20 | 106 | | | * Begin to add tests & test infrastructure | | 107 | +------------+------------------------------------------------------------------------+------------+ 108 | | **0.3.0** | * **BREAKING CHANGE:** Package rename to ``luma.led_matrix`` | 2017/01/19 | 109 | +------------+------------------------------------------------------------------------+------------+ 110 | | **0.2.3** | * Bit-bang version using wiringPi | 2013/01/28 | 111 | +------------+------------------------------------------------------------------------+------------+ 112 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------ 3 | Pull requests (code changes / documentation / typos / feature requests / setup) 4 | are gladly accepted. If you are intending some large-scale changes, please get 5 | in touch first to make sure we're on the same page: try and include a docstring 6 | for any new methods, and try and keep method bodies small, readable and 7 | PEP8-compliant. 8 | 9 | GitHub 10 | ^^^^^^ 11 | The source code is available to clone at: http://github.com/rm-hull/luma.led_matrix 12 | 13 | Contributors 14 | ^^^^^^^^^^^^ 15 | * Thijs Triemstra (@thijstriemstra) 16 | * Jon Carlos (@webmonger) 17 | * Unattributed (@wkapga) 18 | * Taras (@tarasius) 19 | * Brice Parent (@agripo) 20 | * Thomas De Keulenaer (@twdkeule) 21 | * Tero Korpela (@terokorp) 22 | * Qinkang Huang (@pokebox) 23 | * Shawn Woodford (@swoodford) 24 | * Phil Howard (@gadgetoid) 25 | * Petr Kracík (@petrkr) 26 | * Emlyn Corrin (@emlyn) 27 | * Bram Verboom (@bramverb) 28 | * Thanassis Tsiodras (@ttsiodras) 29 | * Marko Vincek (markoaurelije) 30 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | --------------------- 3 | 4 | Copyright (c) 2013-2024 Richard Hull and contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst CHANGES.rst CONTRIBUTING.rst LICENSE.rst tox.ini setup.cfg pyproject.toml pytest.ini .coveragerc 2 | 3 | recursive-include luma *.py 4 | 5 | recursive-include doc * 6 | prune doc/_build 7 | 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | recursive-exclude * *~ 11 | recursive-exclude * .coverage 12 | recursive-exclude * .DS_Store 13 | recursive-exclude * .ropeproject 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | `luma.core `__ **|** 2 | `luma.docs `__ **|** 3 | `luma.emulator `__ **|** 4 | `luma.examples `__ **|** 5 | `luma.lcd `__ **|** 6 | luma.led_matrix **|** 7 | `luma.oled `__ 8 | 9 | Luma.LED_Matrix 10 | =============== 11 | **Display drivers for MAX7219, WS2812, APA102** 12 | 13 | .. image:: https://github.com/rm-hull/luma.led_matrix/workflows/luma.led_matrix/badge.svg?branch=master 14 | :target: https://github.com/rm-hull/luma.led_matrix/actions?workflow=luma.led_matrix 15 | 16 | .. image:: https://coveralls.io/repos/github/rm-hull/luma.led_matrix/badge.svg?branch=master 17 | :target: https://coveralls.io/github/rm-hull/luma.led_matrix?branch=master 18 | 19 | .. image:: https://readthedocs.org/projects/luma-led_matrix/badge/?version=latest 20 | :target: http://luma-led-matrix.readthedocs.io/en/latest/?badge=latest 21 | 22 | .. image:: https://img.shields.io/pypi/pyversions/luma.led_matrix.svg 23 | :target: https://pypi.python.org/pypi/luma.led_matrix 24 | 25 | .. image:: https://img.shields.io/pypi/v/luma.led_matrix.svg 26 | :target: https://pypi.python.org/pypi/luma.led_matrix 27 | 28 | .. image:: https://img.shields.io/pypi/dm/luma.led_matrix 29 | :target: https://pypi.python.org/project/luma.led_matrix 30 | 31 | Python 3 library interfacing LED matrix displays with the MAX7219 driver (using 32 | SPI), WS2812 (NeoPixels, inc Pimoroni Unicorn pHat/Hat and Unicorn Hat HD) and 33 | APA102 (DotStar) on the Raspberry Pi and other Linux-based single board computers 34 | - it provides a `Pillow `_-compatible drawing 35 | canvas, and other functionality to support: 36 | 37 | * multiple cascaded devices 38 | * LED matrix, seven-segment and NeoPixel variants 39 | * scrolling/panning capability, 40 | * terminal-style printing, 41 | * state management, 42 | * dithering to monochrome, 43 | * pygame emulator, 44 | * Python 3.8 and newer are supported 45 | 46 | Documentation 47 | ------------- 48 | Full documentation with installation instructions and examples can be found on https://luma-led-matrix.readthedocs.io. 49 | 50 | .. image:: https://raw.githubusercontent.com/rm-hull/luma.led_matrix/master/doc/images/devices.jpg 51 | :alt: max7219 matrix 52 | 53 | A LED matrix can be acquired for a few pounds from outlets 54 | like `Banggood `_. 55 | Likewise 7-segment displays are available from `Ali-Express 56 | `_ 57 | or `Ebay `_. 58 | 59 | .. image:: https://raw.githubusercontent.com/rm-hull/luma.led_matrix/master/doc/images/IMG_2810.JPG 60 | :alt: max7219 sevensegment 61 | 62 | .. image:: https://raw.githubusercontent.com/rm-hull/luma.led_matrix/master/doc/images/matrix_cascaded.jpg 63 | :alt: max7219 cascaded 64 | 65 | .. image:: https://raw.githubusercontent.com/rm-hull/luma.led_matrix/master/doc/images/box_helloworld.jpg 66 | :alt: max7219 box 67 | 68 | .. image:: https://raw.githubusercontent.com/rm-hull/luma.led_matrix/master/doc/images/emulator.gif 69 | :alt: max7219 emulator 70 | 71 | License 72 | ------- 73 | The MIT License (MIT) 74 | 75 | Copyright (c) 2013-2024 Richard Hull & Contributors 76 | 77 | Permission is hereby granted, free of charge, to any person obtaining a copy 78 | of this software and associated documentation files (the "Software"), to deal 79 | in the Software without restriction, including without limitation the rights 80 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 81 | copies of the Software, and to permit persons to whom the Software is 82 | furnished to do so, subject to the following conditions: 83 | 84 | The above copyright notice and this permission notice shall be included in all 85 | copies or substantial portions of the Software. 86 | 87 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 88 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 89 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 90 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 91 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 92 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 93 | SOFTWARE. 94 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | _static 3 | _templates -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/luma.led_matrix.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/luma.led_matrix.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/luma.led_matrix" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/luma.led_matrix" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /doc/api-documentation.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ----------------- 3 | .. automodule:: luma.led_matrix 4 | :members: 5 | :undoc-members: 6 | :show-inheritance: 7 | 8 | .. inheritance-diagram:: luma.core.device luma.core.mixin luma.core.virtual luma.led_matrix.device 9 | 10 | Upgrading 11 | """"""""" 12 | .. warning:: 13 | Version 0.3.0 was released on 19 January 2017: this came with a rename of the 14 | project in github from **max7219** to **luma.led_matrix** to reflect the changing 15 | nature of the codebase. It introduces a complete rewrite of the codebase to bring 16 | it in line with other 'luma' implementations. 17 | 18 | There is no direct migration path, but the old `documentation `_ 19 | and `PyPi packages `_ will remain 20 | available indefinitely, but that deprecated codebase will no longer recieve 21 | updates or fixes. 22 | 23 | This breaking change was necessary to be able to add different classes of 24 | devices, so that they could reuse core components. 25 | 26 | :mod:`luma.led_matrix.device` 27 | """"""""""""""""""""""""""""" 28 | .. automodule:: luma.led_matrix.device 29 | :members: 30 | :inherited-members: 31 | :undoc-members: 32 | :show-inheritance: 33 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # luma.led_matrix documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Mar 11 23:24:05 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | import sys 17 | from datetime import datetime 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | 23 | sys.path.insert(0, os.path.abspath('..')) 24 | 25 | from luma.led_matrix import __version__ as version 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.doctest', 35 | 'sphinx.ext.intersphinx', 36 | 'sphinx.ext.autosectionlabel', 37 | 'sphinx.ext.todo', 38 | 'sphinx.ext.coverage', 39 | 'sphinx.ext.ifconfig', 40 | 'sphinx.ext.viewcode', 41 | 'sphinx.ext.inheritance_diagram', 42 | 'sphinx.ext.extlinks' 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # The suffix of source filenames. 49 | source_suffix = '.rst' 50 | 51 | # The encoding of source files. 52 | #source_encoding = 'utf-8-sig' 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | # General information about the project. 58 | project = u'Luma.LED_Matrix: Display driver for MAX7219, WS2812, APA102' 59 | author = u'Richard Hull and contributors' 60 | copyright = u'2015-{0}, {1}'.format(datetime.now().year, author) 61 | 62 | # The version info for the project you're documenting, acts as replacement for 63 | # |version| and |release|, also used in various other places throughout the 64 | # built documents. 65 | 66 | # The full version, including alpha/beta/rc tags. 67 | release = version 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | language = 'en' 72 | 73 | # List of patterns, relative to source directory, that match files and 74 | # directories to ignore when looking for source files. 75 | exclude_patterns = ['_build'] 76 | 77 | # The name of the Pygments (syntax highlighting) style to use. 78 | pygments_style = 'sphinx' 79 | 80 | # -- Options for HTML output ---------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | html_theme = 'sphinx_rtd_theme' 85 | 86 | # Output file base name for HTML help builder. 87 | htmlhelp_basename = 'luma.led_matrix_doc' 88 | 89 | # -- Options for LaTeX output --------------------------------------------- 90 | 91 | latex_elements = { 92 | # The paper size ('letterpaper' or 'a4paper'). 93 | #'papersize': 'letterpaper', 94 | 95 | # The font size ('10pt', '11pt' or '12pt'). 96 | #'pointsize': '10pt', 97 | 98 | # Additional stuff for the LaTeX preamble. 99 | #'preamble': '', 100 | } 101 | 102 | # Grouping the document tree into LaTeX files. List of tuples 103 | # (source start file, target name, title, 104 | # author, documentclass [howto, manual, or own class]). 105 | latex_documents = [ 106 | ('index', 'luma.led_matrix.tex', u'Luma.LED_Matrix Documentation', 107 | author, 'manual'), 108 | ] 109 | 110 | # -- Options for manual page output --------------------------------------- 111 | 112 | # One entry per manual page. List of tuples 113 | # (source start file, name, description, authors, manual section). 114 | man_pages = [ 115 | ('index', 'luma.led_matrix', u'Luma.LED_Matrix Documentation', 116 | [author], 1) 117 | ] 118 | 119 | # -- Options for Texinfo output ------------------------------------------- 120 | 121 | # Grouping the document tree into Texinfo files. List of tuples 122 | # (source start file, target name, title, author, 123 | # dir menu entry, description, category) 124 | texinfo_documents = [ 125 | ('index', 'luma.led_matrix', u'Luma.LED_Matrix Documentation', 126 | author, 'luma.led_matrix', 'One line description of project.', 127 | 'Miscellaneous'), 128 | ] 129 | 130 | # Configuration for intersphinx 131 | intersphinx_mapping = { 132 | 'python': ('https://docs.python.org/3', None), 133 | 'pillow': ('https://pillow.readthedocs.io/en/latest', None), 134 | 'luma.core': ('https://luma-core.readthedocs.io/en/latest', None), 135 | 'luma.emulator': ('https://luma-emulator.readthedocs.io/en/latest', None) 136 | } 137 | -------------------------------------------------------------------------------- /doc/fritzing/3x 7segment.fzz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/fritzing/3x 7segment.fzz -------------------------------------------------------------------------------- /doc/images/7-segment.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /doc/images/7segment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/images/7segment.jpg -------------------------------------------------------------------------------- /doc/images/BL-M12A881.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/images/BL-M12A881.png -------------------------------------------------------------------------------- /doc/images/IMG_2810.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/images/IMG_2810.JPG -------------------------------------------------------------------------------- /doc/images/block_reorientation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/images/block_reorientation.gif -------------------------------------------------------------------------------- /doc/images/box_helloworld.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/images/box_helloworld.jpg -------------------------------------------------------------------------------- /doc/images/devices.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/images/devices.jpg -------------------------------------------------------------------------------- /doc/images/emulator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/images/emulator.gif -------------------------------------------------------------------------------- /doc/images/level-shifter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/images/level-shifter.jpg -------------------------------------------------------------------------------- /doc/images/matrix.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/images/matrix.jpg -------------------------------------------------------------------------------- /doc/images/matrix_cascaded.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/images/matrix_cascaded.jpg -------------------------------------------------------------------------------- /doc/images/raspi-spi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/images/raspi-spi.png -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | Luma.LED_Matrix: Display drivers for MAX7219, WS2812, APA102 2 | ============================================================ 3 | 4 | .. image:: https://github.com/rm-hull/luma.led_matrix/workflows/luma.led_matrix/badge.svg?branch=master 5 | :target: https://github.com/rm-hull/luma.led_matrix/actions?workflow=luma.led_matrix 6 | 7 | .. image:: https://coveralls.io/repos/github/rm-hull/luma.led_matrix/badge.svg?branch=master 8 | :target: https://coveralls.io/github/rm-hull/luma.led_matrix?branch=master 9 | 10 | .. image:: https://readthedocs.org/projects/luma-led_matrix/badge/?version=latest 11 | :target: http://luma-led-matrix.readthedocs.io/en/latest/?badge=latest 12 | 13 | .. image:: https://img.shields.io/pypi/pyversions/luma.led_matrix.svg 14 | :target: https://pypi.python.org/pypi/luma.led_matrix 15 | 16 | .. image:: https://img.shields.io/pypi/v/luma.led_matrix.svg 17 | :target: https://pypi.python.org/pypi/luma.led_matrix 18 | 19 | .. image:: https://img.shields.io/pypi/dm/luma.led_matrix 20 | :target: https://pypi.python.org/project/luma.led_matrix 21 | 22 | .. toctree:: 23 | :maxdepth: 2 24 | 25 | intro 26 | install 27 | python-usage 28 | api-documentation 29 | notes 30 | references 31 | 32 | .. include:: ../CONTRIBUTING.rst 33 | .. include:: ../CHANGES.rst 34 | .. include:: ../LICENSE.rst 35 | -------------------------------------------------------------------------------- /doc/install.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ------------ 3 | .. note:: The library has been tested against Python 3.6 and newer. 4 | 5 | Pre-requisites 6 | ^^^^^^^^^^^^^^ 7 | 8 | MAX7219 Devices 9 | """"""""""""""" 10 | By default, the SPI kernel driver is **NOT** enabled on a Raspberry Pi Raspbian image. 11 | You can confirm whether it is enabled using the shell command below:: 12 | 13 | $ lsmod | grep -i spi 14 | spi_bcm2835 7424 0 15 | 16 | Depending on the hardware/kernel version, this may report **spi_bcm2807** rather 17 | than **spi_bcm2835** - either should be adequate. 18 | 19 | And to verify that the devices are successfully installed in ``/dev``:: 20 | 21 | $ ls -l /dev/spi* 22 | crw------- 1 root root 153, 0 Jan 1 1970 /dev/spidev0.0 23 | crw------- 1 root root 153, 1 Jan 1 1970 /dev/spidev0.1 24 | 25 | If you have no ``/dev/spi`` files and nothing is showing using ``lsmod`` then this 26 | implies the kernel SPI driver is not loaded. Enable the SPI as follows (steps 27 | taken from https://learn.sparkfun.com/tutorials/raspberry-pi-spi-and-i2c-tutorial#spi-on-pi): 28 | 29 | #. Run ``sudo raspi-config`` 30 | #. Use the down arrow to select ``5 Interfacing Options`` 31 | #. Arrow down to ``P4 SPI`` 32 | #. Select **yes** when it asks you to enable SPI 33 | #. Also select **yes** when it asks about automatically loading the kernel module 34 | #. Use the right arrow to select the **** button 35 | #. Reboot. 36 | 37 | .. image:: images/raspi-spi.png 38 | 39 | After rebooting re-check that the ``lsmod | grep -i spi`` command shows whether 40 | SPI driver is loaded before proceeding. If you are stil experiencing problems, refer to the official 41 | Raspberry Pi `SPI troubleshooting guide `_ 42 | for further details, or ask a `new question `_ - but 43 | please remember to add as much detail as possible. 44 | 45 | GPIO pin-outs 46 | ^^^^^^^^^^^^^ 47 | 48 | MAX7219 Devices (SPI) 49 | """"""""""""""""""""" 50 | The breakout board has two headers to allow daisy-chaining: 51 | 52 | ============ ====== ============= ========= ==================== 53 | Board Pin Name Remarks RPi Pin RPi Function 54 | ------------ ------ ------------- --------- -------------------- 55 | 1 VCC +5V Power 2 5V0 56 | 2 GND Ground 6 GND 57 | 3 DIN Data In 19 GPIO 10 (MOSI) 58 | 4 CS Chip Select 24 GPIO 8 (SPI CE0) 59 | 5 CLK Clock 23 GPIO 11 (SPI CLK) 60 | ============ ====== ============= ========= ==================== 61 | 62 | .. seealso:: Also see the section for :doc:`cascading/daisy-chaining `, power supply and 63 | level-shifting. 64 | 65 | WS2812 NeoPixels (DMA) 66 | """""""""""""""""""""" 67 | Typically, WS2812 NeoPixels reqire VCC, VSS (GND) and DI pins connecting to the 68 | Raspberry Pi, where the DI pin is usually connected to a PWM control pin such 69 | as GPIO 18. 70 | 71 | ============ ====== ============= ========= ==================== 72 | Board Pin Name Remarks RPi Pin RPi Function 73 | ------------ ------ ------------- --------- -------------------- 74 | 1 DO Data Out - - 75 | 2 DI Data In 12 GPIO 18 (PWM0) 76 | 3 VCC +5V Power 2 5V0 77 | 4 NC Not connected - - 78 | 5 VDD Not connected - - 79 | 6 VSS Ground 6 GND 80 | ============ ====== ============= ========= ==================== 81 | 82 | The DO pin should be connected to the DI pin on the next (daisy-chained) 83 | neopixel, while the VCC and VSS are supplied in-parallel to all LED's. 84 | WS2812b devices now are becoming more prevalent, and only have 4 pins. 85 | 86 | NeoSegments 87 | """"""""""" 88 | @msurguy's NeoSegments should be connected as follows: 89 | 90 | ============ ====== ============= ========= ==================== 91 | Board Pin Name Remarks RPi Pin RPi Function 92 | ------------ ------ ------------- --------- -------------------- 93 | 1 GND Ground 6 GND 94 | 2 DI Data In 12 GPIO 18 (PWM0) 95 | 3 VCC +5V Power 2 5V0 96 | ============ ====== ============= ========= ==================== 97 | 98 | 99 | Installing from PyPi 100 | ^^^^^^^^^^^^^^^^^^^^ 101 | Install the dependencies for library first with:: 102 | 103 | $ sudo usermod -a -G spi,gpio pi 104 | $ sudo apt install build-essential python3-dev python3-pip libfreetype6-dev libjpeg-dev libopenjp2-7 libtiff5 105 | 106 | .. warning:: The default ``pip`` and ``setuptools`` bundled with apt on Raspbian are really old, 107 | and can cause components to not be installed properly. Make sure they are up to date by upgrading 108 | them first:: 109 | 110 | $ sudo -H pip install --upgrade --ignore-installed pip setuptools 111 | 112 | Proceed to install latest version of the luma.led_matrix library directly from 113 | `PyPI `_:: 114 | 115 | $ sudo python3 -m pip install --upgrade luma.led_matrix 116 | 117 | Examples 118 | ^^^^^^^^ 119 | Ensure you have followed the installation instructions above. 120 | Clone the `repo `__ from github, 121 | and run the example code as follows:: 122 | 123 | $ python examples/matrix_demo.py 124 | 125 | The matrix demo accepts optional flags to configure the number of cascaded 126 | devices and correct the block orientation phase shift when using 4x8x8 127 | matrices:: 128 | 129 | $ python examples/matrix_demo.py -h 130 | usage: matrix_demo.py [-h] [--cascaded CASCADED] 131 | [--block-orientation {0,90,-90}] [--rotate {0,1,2,3}] 132 | [--reverse-order REVERSE_ORDER] 133 | 134 | matrix_demo arguments 135 | 136 | optional arguments: 137 | -h, --help show this help message and exit 138 | --cascaded CASCADED, -n CASCADED 139 | Number of cascaded MAX7219 LED matrices (default: 1) 140 | --block-orientation {0,90,-90} 141 | Corrects block orientation when wired vertically 142 | (default: 0) 143 | --rotate {0,1,2,3} Rotate display 0=0_, 1=90_, 2=180_, 3=270_ 144 | (default: 0) 145 | --reverse-order REVERSE_ORDER 146 | Set to true if blocks are in reverse order (default: 147 | False) 148 | 149 | Similarly, there is a basic demo of the capabilities of the 150 | :py:class:`luma.led_matrix.virtual.sevensegment` wrapper:: 151 | 152 | $ python examples/sevensegment_demo.py 153 | 154 | and for the :py:class:`luma.led_matrix.device.neopixel` device:: 155 | 156 | $ sudo python examples/neopixel_demo.py 157 | 158 | Further examples are available in the `luma.examples 159 | `_. git repository. Follow the 160 | instructions in the README for more details. 161 | 162 | A small example application using `ZeroSeg 163 | `_ to display TOTP secrets can be 164 | found in https://github.com/rm-hull/zaup. 165 | -------------------------------------------------------------------------------- /doc/intro.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ------------ 3 | Python library interfacing LED matrix displays with the MAX7219 driver (using 4 | SPI) and WS2812 & APA102 NeoPixels (inc Pimoroni Unicorn pHat/Hat and Unicorn 5 | Hat HD) on the Raspberry Pi and other Linux-based single board computers - it 6 | provides a Pillow-compatible drawing canvas, and other functionality to 7 | support: 8 | 9 | * multiple cascaded devices 10 | * LED matrix, seven-segment and NeoPixel variants 11 | * scrolling/panning capability, 12 | * terminal-style printing, 13 | * state management, 14 | * dithering to monochrome, 15 | * Python 3.8+ is supported 16 | 17 | .. image:: https://raw.githubusercontent.com/rm-hull/luma.led_matrix/master/doc/images/devices.jpg 18 | :alt: max7219 matrix 19 | 20 | A LED matrix can be acquired for a few pounds from outlets like `Banggood 21 | `_. 22 | Likewise 7-segment displays are available from `Ali-Express 23 | `_ 24 | or `Ebay `_. 25 | 26 | .. seealso:: 27 | Further technical information for the specific devices can be found in the 28 | datasheets below: 29 | 30 | - :download:`MAX7219 ` 31 | - :download:`WS2812 ` 32 | - :download:`WS2812B ` 33 | - :download:`APA102 ` 34 | -------------------------------------------------------------------------------- /doc/notes.rst: -------------------------------------------------------------------------------- 1 | Notes 2 | ----- 3 | 4 | Cascading, power supply & level shifting 5 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | The MAX7219 chip supports cascading devices by connecting the DIN of one chip 7 | to the DOUT of another chip. For a long time I was puzzled as to why this didnt 8 | seem to work properly for me, despite spending a lot of time investigating and 9 | always assuming it was a bug in code. 10 | 11 | - Because the Raspberry PI can only supply a limited amount of power from the 12 | 5V rail, it is recommended that any LED matrices are powered separately by a 13 | 5V supply, and grounded with the Raspberry PI. It is possible to power one or 14 | two LED matrices directly from a Raspberry PI, but any more is likely to 15 | cause intermittent faults & crashes. 16 | 17 | - Also because the GPIO ports used for SPI are 3.3V, a simple level shifter (as 18 | per the diagram below) should be employed on the DIN, CS and CLK inputs to 19 | boost the levels to 5V. Again it is possible to drive them directly by the 20 | 3.3V GPIO pins, it is just outside tolerance, and will result in intermittent 21 | issues. 22 | 23 | .. image:: images/level-shifter.jpg 24 | :alt: max7219 levelshifter 25 | 26 | Despite the above two points, I still had no success getting cascaded matrices 27 | to work properly. Revisiting the wiring, I had connected the devices in serial 28 | connecting the out pins of one device to the in pins of another. This just 29 | produced garbled bit patterns. 30 | 31 | Connecting all the CS lines on the input side together and CLK lines on the 32 | input side all together worked. The same should probably apply to GND and VCC 33 | respectively: Only the DOUT of one device should be connected to the next 34 | devices DIN pins. Connecting through the output side, never worked 35 | consistently; I can only assume that there is some noise on the clock line, or 36 | a dry solder joint somewhere. 37 | 38 | .. image:: images/matrix_cascaded.jpg 39 | :alt: max7219 cascaded 40 | -------------------------------------------------------------------------------- /doc/python-usage.rst: -------------------------------------------------------------------------------- 1 | Python Usage 2 | ------------ 3 | 4 | 8x8 LED Matrices 5 | ^^^^^^^^^^^^^^^^ 6 | For the matrix device, initialize the :py:class:`luma.led_matrix.device.max7219` 7 | class, as follows: 8 | 9 | .. code:: python 10 | 11 | from luma.core.interface.serial import spi, noop 12 | from luma.core.render import canvas 13 | from luma.led_matrix.device import max7219 14 | 15 | serial = spi(port=0, device=0, gpio=noop()) 16 | device = max7219(serial) 17 | 18 | The display device should now be configured for use. The specific 19 | :py:class:`~luma.led_matrix.device.max7219` class exposes a 20 | :py:func:`~luma.led_matrix.device.max7219.display` method which takes an image 21 | with attributes consistent with the capabilities of the configured device's 22 | capabilities. However, for most cases, for drawing text and graphics primitives, 23 | the canvas class should be used as follows: 24 | 25 | .. code:: python 26 | 27 | from PIL import ImageFont 28 | 29 | font = ImageFont.truetype("examples/pixelmix.ttf", 8) 30 | 31 | with canvas(device) as draw: 32 | draw.rectangle(device.bounding_box, outline="white", fill="black") 33 | 34 | The :py:class:`luma.core.render.canvas` class automatically creates an 35 | :py:mod:`PIL.ImageDraw` object of the correct dimensions and bit depth suitable 36 | for the device, so you may then call the usual Pillow methods to draw onto the 37 | canvas. 38 | 39 | As soon as the with scope is ended, the resultant image is automatically 40 | flushed to the device's display memory and the :mod:`PIL.ImageDraw` object is 41 | garbage collected. 42 | 43 | .. note:: 44 | The default Pillow font is too big for 8px high devices like the LED matrices 45 | here, so the `luma.examples `_ repo 46 | inclues a small TTF pixel font called **pixelmix.ttf** (attribution: 47 | http://www.dafont.com/) which just fits. 48 | 49 | Alternatively, a set of "legacy" fixed-width bitmap fonts are included in 50 | the `luma.core `__ codebase and may be 51 | used as follows: 52 | 53 | .. code:: python 54 | 55 | from luma.core.legacy import text 56 | from luma.core.legacy.font import proportional, CP437_FONT, LCD_FONT 57 | 58 | with canvas(device) as draw: 59 | text(draw, (0, 0), "A", fill="white", font=proportional(CP437_FONT)) 60 | 61 | The fixed-width fonts can be "converted" on-the-fly to proportionally 62 | spaced by wrapping them with the :py:class:`luma.core.legacy.font.proportional` 63 | class. 64 | 65 | Scrolling / Virtual viewports 66 | """"""""""""""""""""""""""""" 67 | A single 8x8 LED matrix clearly hasn't got a lot of area for displaying useful 68 | information. Obviously they can be daisy-chained together to provide a longer 69 | line of text, but as this library extends `luma.core `_, 70 | then we can use the :py:class:`luma.core.virtual.viewport` class to allow 71 | scrolling support: 72 | 73 | .. code:: python 74 | 75 | import time 76 | 77 | from luma.core.interface.serial import spi, noop 78 | from luma.core.render import canvas 79 | from luma.core.virtual import viewport 80 | from luma.led_matrix.device import max7219 81 | 82 | serial = spi(port=0, device=0, gpio=noop()) 83 | device = max7219(serial) 84 | 85 | virtual = viewport(device, width=200, height=100) 86 | 87 | with canvas(virtual) as draw: 88 | draw.rectangle(device.bounding_box, outline="white", fill="black") 89 | draw.text((3, 3), "Hello world", fill="white") 90 | 91 | for offset in range(8): 92 | virtual.set_position((offset, offset)) 93 | time.sleep(0.1) 94 | 95 | Calling :py:meth:`~luma.core.virtual.viewport.set_position` on a virtual 96 | viewport, causes the device to render what is visible at that specific 97 | position; altering the position in a loop refreshes every time it is called, 98 | and gives an animated scrolling effect. 99 | 100 | By altering both the X and Y co-ordinates allows scrolling in any direction, 101 | not just horizontally. 102 | 103 | Color Model 104 | """"""""""" 105 | Any of the standard :mod:`PIL.ImageColor` color formats may be used, but since 106 | the 8x8 LED Matrices are monochrome, only the HTML color names :py:const:`"black"` and 107 | :py:const:`"white"` values should really be used; in fact, by default, any value 108 | *other* than black is treated as white. The :py:class:`luma.core.render.canvas` 109 | constructor does have a :py:attr:`dither` flag which if set to 110 | :py:const:`True`, will convert color drawings to a dithered monochrome effect. 111 | 112 | .. code:: python 113 | 114 | with canvas(device, dither=True) as draw: 115 | draw.rectangle(device.bounding_box, outline="white", fill="red") 116 | 117 | Landscape / Portrait Orientation 118 | """""""""""""""""""""""""""""""" 119 | By default, cascaded matrices will be oriented in landscape mode. Should you 120 | have an application that requires the display to be mounted in a portrait 121 | aspect, then add a :py:attr:`rotate=N` parameter when creating the device: 122 | 123 | .. code:: python 124 | 125 | from luma.core.interface.serial import spi, noop 126 | from luma.core.render import canvas 127 | from luma.led_matrix.device import max7219 128 | 129 | serial = spi(port=0, device=0, gpio=noop()) 130 | device = max7219(serial, rotate=1) 131 | 132 | # Box and text rendered in portrait mode 133 | with canvas(device) as draw: 134 | draw.rectangle(device.bounding_box, outline="white", fill="black") 135 | 136 | *N* should be a value of 0, 1, 2 or 3 only, where 0 is no rotation, 1 is 137 | rotate 90° clockwise, 2 is 180° rotation and 3 represents 270° rotation. 138 | 139 | The :py:attr:`device.size`, :py:attr:`device.width` and :py:attr:`device.height` 140 | properties reflect the rotated dimensions rather than the physical dimensions. 141 | 142 | Daisy-chaining 143 | """""""""""""" 144 | The MAX7219 chipset supports a serial 16-bit register/data buffer which is 145 | clocked in on pin DIN every time the clock edge falls, and clocked out on DOUT 146 | 16.5 clock cycles later. This allows multiple devices to be chained together. 147 | 148 | If you have more than one device and they are daisy-chained together, you can 149 | initialize the library in one of two ways, either using :py:attr:`cascaded=N` 150 | to indicate the number of daisychained devices: 151 | 152 | .. code:: python 153 | 154 | from luma.core.interface.serial import spi, noop 155 | from luma.core.render import canvas 156 | from luma.led_matrix.device import max7219 157 | 158 | serial = spi(port=0, device=0, gpio=noop()) 159 | device = max7219(serial, cascaded=3) 160 | 161 | with canvas(device) as draw: 162 | draw.rectangle(device.bounding_box, outline="white", fill="black") 163 | 164 | Using :py:attr:`cascaded=N` implies there are N devices arranged linearly and 165 | horizontally, running left to right. 166 | 167 | Alternatively, the device configuration may configured with :py:attr:`width=W` 168 | and :py:attr:`height=H`. These dimensions denote the number of LEDs in the all 169 | the daisychained devices. The width and height *must* both be multiples of 8: 170 | this has scope for arranging in blocks in, say 3x3 or 5x2 matrices (24x24 or 171 | 40x16 pixels, respectively). 172 | 173 | Given 12 daisychained MAX7219's arranged in a 4x3 layout, the simple example 174 | below, 175 | 176 | .. code:: python 177 | 178 | from luma.core.interface.serial import spi, noop 179 | from luma.core.render import canvas 180 | from luma.core.legacy import text 181 | from luma.core.legacy.font import proportional, LCD_FONT 182 | from luma.led_matrix.device import max7219 183 | 184 | serial = spi(port=0, device=0, gpio=noop()) 185 | device = max7219(serial, width=32, height=24, block_orientation=-90) 186 | 187 | with canvas(device) as draw: 188 | draw.rectangle(device.bounding_box, outline="white") 189 | text(draw, (2, 2), "Hello", fill="white", font=proportional(LCD_FONT)) 190 | text(draw, (2, 10), "World", fill="white", font=proportional(LCD_FONT)) 191 | 192 | displays as: 193 | 194 | .. image:: images/box_helloworld.jpg 195 | :alt: box helloworld 196 | 197 | 198 | Trouble-shooting / common problems 199 | """""""""""""""""""""""""""""""""" 200 | Some online retailers are selling pre-assembled `'4-in-1' LED matrix displays 201 | `_, but they appear to be wired 90° 202 | out-of-phase such that horizontal scrolling appears as below: 203 | 204 | .. image:: images/block_reorientation.gif 205 | :alt: block alignment 206 | 207 | This can be rectified by initializing the :py:class:`~luma.led_matrix.device.max7219` 208 | device with a parameter of :py:attr:`block_orientation=-90` (or +90, if your device is 209 | aligned the other way): 210 | 211 | .. code:: python 212 | 213 | from luma.core.interface.serial import spi, noop 214 | from luma.core.render import canvas 215 | from luma.led_matrix.device import max7219 216 | 217 | serial = spi(port=0, device=0, gpio=noop()) 218 | device = max7219(serial, cascaded=4, block_orientation=-90) 219 | 220 | Every time a display render is subsequenly requested, the underlying image 221 | representation is corrected to reverse the 90° phase shift. 222 | 223 | Similarly, in other pre-assembled configurations, the 4-in-1 blocks 224 | arrange the 8x8 blocks in reverse order. In that case, you need to pass 225 | a True value to parameter `blocks_arranged_in_reverse_order`, requesting 226 | an additional pre-processing step that fixes this: 227 | 228 | .. code:: python 229 | 230 | ... 231 | device = max7219(serial, cascaded=4, block_orientation=-90, 232 | blocks_arranged_in_reverse_order=True) 233 | 234 | 7-Segment LED Displays 235 | ^^^^^^^^^^^^^^^^^^^^^^ 236 | For the 7-segment device, initialize the :py:class:`luma.core.virtual.sevensegment` 237 | class, and wrap it around a previously created :py:class:`~luma.led_matrix.device.max7219` 238 | device: 239 | 240 | .. code:: python 241 | 242 | from luma.core.interface.serial import spi, noop 243 | from luma.core.render import canvas 244 | from luma.core.virtual import sevensegment 245 | from luma.led_matrix.device import max7219 246 | 247 | serial = spi(port=0, device=0, gpio=noop()) 248 | device = max7219(serial, cascaded=2) 249 | seg = sevensegment(device) 250 | 251 | The **seg** instance now has a :py:attr:`~luma.core.virtual.sevensegment.text` 252 | property which may be assigned, and when it does will update all digits 253 | according to the limited alphabet the 7-segment displays support. For example, 254 | assuming there are 2 cascaded modules, we have 16 character available, and so 255 | can write: 256 | 257 | .. code:: python 258 | 259 | seg.text = "Hello world" 260 | 261 | Rather than updating the whole display buffer, it is possible to update 262 | 'slices', as per the below example: 263 | 264 | .. code:: python 265 | 266 | seg.text[0:5] = "Goodbye" 267 | 268 | This replaces ``Hello`` in the previous example, replacing it with ``Gooobye``. 269 | The usual python idioms for slicing (inserting / replacing / deleteing) can be 270 | used here, but note if inserted text exceeds the underlying buffer size, a 271 | :py:exc:`ValueError` is raised. 272 | 273 | Floating point numbers (or text with '.') are handled slightly differently - the 274 | decimal-place is fused in place on the character immediately preceding it. This 275 | means that it is technically possible to get more characters displayed than the 276 | buffer allows, but only because dots are folded into their host character 277 | 278 | .. image:: images/IMG_2810.JPG 279 | :alt: max7219 sevensegment 280 | 281 | WS2812 NeoPixels 282 | ^^^^^^^^^^^^^^^^ 283 | For a strip of neopixels, initialize the :py:class:`luma.led_matrix.device.ws2812` 284 | class (also aliased to :py:class:`luma.led_matrix.device.neopixel`), supplying a 285 | parameter :py:attr:`cascaded=N` where *N* is the number of daisy-chained LEDs. 286 | 287 | This script creates a drawing surface 100 pixels long, and lights up three specific 288 | pixels, and a contiguous block: 289 | 290 | .. code:: python 291 | 292 | from luma.core.render import canvas 293 | from luma.led_matrix.device import ws2812 294 | 295 | device = ws2812(cascaded=100) 296 | 297 | with canvas(device) as draw: 298 | draw.point((0,0), fill="white") 299 | draw.point((4,0), fill="blue") 300 | draw.point((11,0), fill="orange") 301 | draw.rectange((20, 0, 40, 0), fill="red") 302 | 303 | If you have a device like Pimoroni's `Unicorn pHat `_, 304 | initialize the device with :py:attr:`width=N` and :py:attr:`height=N` attributes instead: 305 | 306 | .. code:: python 307 | 308 | from luma.core.render import canvas 309 | from luma.led_matrix.device import ws2812 310 | 311 | # Pimoroni's Unicorn pHat is 8x4 neopixels 312 | device = ws2812(width=8, height=4) 313 | 314 | with canvas(device) as draw: 315 | draw.line((0, 0, 0, device.height), fill="red") 316 | draw.line((1, 0, 1, device.height), fill="orange") 317 | draw.line((2, 0, 2, device.height), fill="yellow") 318 | draw.line((3, 0, 3, device.height), fill="green") 319 | draw.line((4, 0, 4, device.height), fill="blue") 320 | draw.line((5, 0, 5, device.height), fill="indigo") 321 | draw.line((6, 0, 6, device.height), fill="violet") 322 | draw.line((7, 0, 7, device.height), fill="white") 323 | 324 | .. note:: 325 | The ws2812 driver uses the `ws2812 `_ 326 | PyPi package to interface to the daisychained LEDs. It uses DMA (direct memory 327 | access) via ``/dev/mem`` which means that it has to run in privileged mode 328 | (via ``sudo`` root access). 329 | 330 | The same viewport, scroll support, portrait/landscape orientation and color model 331 | idioms provided in luma.core are equally applicable to the ws2812 implementation. 332 | 333 | Pimoroni Unicorn HAT 334 | """""""""""""""""""" 335 | Pimoroni sells the `Unicorn HAT `_, 336 | comprising 64 WS2812b NeoPixels in an 8x8 arrangement. The pixels are cascaded, but 337 | arranged in a 'snake' layout, rather than a 'scan' layout. In order to accomodate this, 338 | a translation mapping is required, as follows: 339 | 340 | .. code:: python 341 | 342 | import time 343 | 344 | from luma.led_matrix.device import ws2812, UNICORN_HAT 345 | from luma.core.render import canvas 346 | 347 | device = ws2812(width=8, height=8, mapping=UNICORN_HAT) 348 | 349 | for y in range(device.height): 350 | for x in range(device.width): 351 | with canvas(device) as draw: 352 | draw.point((x, y), fill="green") 353 | time.sleep(0.5) 354 | 355 | This should animate a green dot moving left-to-right down each line. 356 | 357 | Pimoroni Unicorn HAT HD 358 | """"""""""""""""""""""" 359 | Pimoroni sells the `Unicorn HAT HD `_, 360 | comprising 256 high-intensity RGB LEDs in a 16x16 arrangement. The pixels are driven by an 361 | ARM STM32F making the display appear as an SPI device: 362 | 363 | .. code:: python 364 | 365 | import time 366 | 367 | from luma.led_matrix.device import unicornhathd 368 | from luma.core.interface.serial import spi, noop 369 | from luma.core.render import canvas 370 | 371 | serial = spi(port=0, device=0, gpio=noop()) 372 | device = unicornhathd(serial) 373 | 374 | for y in range(device.height): 375 | for x in range(device.width): 376 | with canvas(device) as draw: 377 | draw.point((x, y), fill="green") 378 | time.sleep(0.5) 379 | 380 | This should animate a green dot moving left-to-right down each line. 381 | 382 | NeoSegments (WS2812) 383 | """""""""""""""""""" 384 | `@msurguy `_ has `crowdsourced some WS2812 neopixels `_ 385 | into a modular 3D-printed seven-segment unit. To program these devices: 386 | 387 | .. code:: python 388 | 389 | import time 390 | 391 | from luma.led_matrix_device import neosegment 392 | 393 | neoseg = neosegment(width=6) 394 | 395 | # Defaults to "white" color initially 396 | neoseg.text = "NEOSEG" 397 | time.sleep(1) 398 | 399 | # Set the first char ('N') to red 400 | neoseg.color[0] = "red" 401 | time.sleep(1) 402 | 403 | # Set fourth and fifth chars ('S','E') accordingly 404 | neoseg.color[3:5] = ["cyan", "blue"] 405 | time.sleep(1) 406 | 407 | # Set the entire string to green 408 | neoseg.color = "green" 409 | 410 | The :py:class:`~luma.led_matrix.device.neosegment` class extends :py:class:`~luma.core.virtual.sevensegment`, 411 | so the same text assignment (Python slicing paradigms) can be used here as well - 412 | see the earlier section for further details. 413 | 414 | The underlying device is exposed as attribute :py:attr:`device`, so methods 415 | such as :py:attr:`show`, :py:attr:`hide` and :py:attr:`contrast` are available. 416 | 417 | Next-generation APA102 NeoPixels 418 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 419 | APA102 RGB neopixels are easier to control that WS2812 devices - they are driven 420 | using SPI rather than precise timings that the WS2812 devices need. Initialize the 421 | :py:class:`luma.led_matrix.device.apa102` class, supplying a parameter 422 | :py:attr:`cascaded=N` where *N* is the number of daisy-chained LEDs. 423 | 424 | The following script creates a drawing surface 8 pixels long, and lights up three 425 | specific pixels: 426 | 427 | .. code:: python 428 | 429 | from luma.core.render import canvas 430 | from luma.led_matrix.device import apa102 431 | 432 | device = apa102(cascaded=8) 433 | 434 | with canvas(device) as draw: 435 | draw.point((0,0), fill="white") 436 | draw.point((0,1), fill="blue") 437 | draw.point((0,2), fill=(0xFF, 0x00, 0x00, 0x80)) # RGBA tuple, alpha controls brightness 438 | 439 | APA102 RGB pixels can have their brightness individually controlled: by setting 440 | the alpha chanel to a translucent value (as per the above example) will set the 441 | brightness accordingly. 442 | 443 | Emulators 444 | ^^^^^^^^^ 445 | There are various `display emulators `_ 446 | available for running code against, for debugging and screen capture functionality: 447 | 448 | * The :py:class:`luma.emulator.device.capture` device will persist a numbered 449 | PNG file to disk every time its :py:meth:`~luma.emulator.device.capture.display` 450 | method is called. 451 | 452 | * The :py:class:`luma.emulator.device.gifanim` device will record every image 453 | when its :py:meth:`~luma.emulator.device.gifanim.display` method is called, 454 | and on program exit (or Ctrl-C), will assemble the images into an animated 455 | GIF. 456 | 457 | * The :py:class:`luma.emulator.device.pygame` device uses the :py:mod:`pygame` 458 | library to render the displayed image to a pygame display surface. 459 | 460 | Invoke the demos with:: 461 | 462 | $ python examples/clock.py -d capture --transform=led_matrix 463 | 464 | or:: 465 | 466 | $ python examples/clock.py -d pygame --transform=led_matrix 467 | 468 | .. note:: 469 | *Pygame* is required to use any of the emulated devices, but it is **NOT** 470 | installed as a dependency by default, and so must be manually installed 471 | before using any of these emulation devices (e.g. ``pip install pygame``). 472 | See the install instructions in `luma.emulator `_ 473 | for further details. 474 | 475 | 476 | .. image:: images/emulator.gif 477 | :alt: max7219 emulator 478 | 479 | -------------------------------------------------------------------------------- /doc/references.rst: -------------------------------------------------------------------------------- 1 | References 2 | ---------- 3 | 4 | - http://hackaday.com/2013/01/06/hardware-spi-with-python-on-a-raspberry-pi/ 5 | - http://gammon.com.au/forum/?id=11516 6 | - http://louisthiery.com/spi-python-hardware-spi-for-raspi/ 7 | - http://www.brianhensley.net/2012/07/getting-spi-working-on-raspberry-pi.html 8 | - http://raspi.tv/2013/8-x-8-led-array-driven-by-max7219-on-the-raspberry-pi-via-python 9 | - http://quick2wire.com/non-root-access-to-spi-on-the-pi 10 | 11 | 12 | -------------------------------------------------------------------------------- /doc/tech-spec/APA102.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/tech-spec/APA102.pdf -------------------------------------------------------------------------------- /doc/tech-spec/MAX7219.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/tech-spec/MAX7219.pdf -------------------------------------------------------------------------------- /doc/tech-spec/TM1637.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/tech-spec/TM1637.pdf -------------------------------------------------------------------------------- /doc/tech-spec/WS2812.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/tech-spec/WS2812.pdf -------------------------------------------------------------------------------- /doc/tech-spec/WS2812B.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/doc/tech-spec/WS2812B.pdf -------------------------------------------------------------------------------- /examples/apa102_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2017-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | import time 7 | 8 | from luma.led_matrix.device import apa102 9 | from luma.core.render import canvas 10 | 11 | device = apa102(width=8, height=1) 12 | 13 | 14 | def rotate(l): 15 | return l[-1:] + l[:-1] 16 | 17 | 18 | def main(): 19 | colors = [ 20 | "red", 21 | "orange", 22 | "yellow", 23 | "green", 24 | "blue", 25 | "indigo", 26 | "violet", 27 | "white" 28 | ] 29 | 30 | for color in colors: 31 | with canvas(device) as draw: 32 | draw.line(device.bounding_box, fill=color) 33 | time.sleep(2) 34 | 35 | device.contrast(0x30) 36 | for _ in range(80): 37 | with canvas(device) as draw: 38 | for x, color in enumerate(colors): 39 | draw.point((x, 0), fill=color) 40 | 41 | colors = rotate(colors) 42 | time.sleep(0.2) 43 | 44 | time.sleep(4) 45 | 46 | device.contrast(0x80) 47 | time.sleep(1) 48 | device.contrast(0x10) 49 | time.sleep(1) 50 | 51 | 52 | if __name__ == "__main__": 53 | try: 54 | main() 55 | except KeyboardInterrupt: 56 | pass 57 | -------------------------------------------------------------------------------- /examples/box_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2017-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | import time 7 | import argparse 8 | 9 | from luma.led_matrix.device import max7219 10 | from luma.core.interface.serial import spi, noop 11 | from luma.core.render import canvas 12 | from luma.core.legacy import text 13 | from luma.core.legacy.font import proportional, LCD_FONT 14 | 15 | 16 | def demo(w, h, block_orientation, rotate): 17 | # create matrix device 18 | serial = spi(port=0, device=0, gpio=noop()) 19 | device = max7219(serial, width=w, height=h, rotate=rotate, block_orientation=block_orientation) 20 | print("Created device") 21 | 22 | with canvas(device) as draw: 23 | draw.rectangle(device.bounding_box, outline="white") 24 | text(draw, (2, 2), "Hello", fill="white", font=proportional(LCD_FONT)) 25 | text(draw, (2, 10), "World", fill="white", font=proportional(LCD_FONT)) 26 | 27 | time.sleep(300) 28 | 29 | 30 | if __name__ == "__main__": 31 | parser = argparse.ArgumentParser(description='matrix_demo arguments', 32 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 33 | 34 | parser.add_argument('--width', type=int, default=8, help='Width') 35 | parser.add_argument('--height', type=int, default=8, help='height') 36 | parser.add_argument('--block-orientation', type=int, default=-90, choices=[0, 90, -90], help='Corrects block orientation when wired vertically') 37 | parser.add_argument('--rotate', type=int, default=0, choices=[0, 1, 2, 3], help='Rotation factor') 38 | 39 | args = parser.parse_args() 40 | 41 | try: 42 | demo(args.width, args.height, args.block_orientation, args.rotate) 43 | except KeyboardInterrupt: 44 | pass 45 | -------------------------------------------------------------------------------- /examples/issue_108.py: -------------------------------------------------------------------------------- 1 | #!usr/bin/env python 2 | 3 | import time 4 | import argparse 5 | 6 | from luma.led_matrix.device import max7219 7 | from luma.core.interface.serial import spi, noop 8 | from luma.core.render import canvas 9 | from luma.core.legacy import text 10 | 11 | print('Press Ctrl-C to quit...') 12 | 13 | serial = spi(port=0, device=0, gpio=noop()) 14 | device = max7219(serial, cascaded=5, block_orientation=0) 15 | 16 | currentLoop = 0 17 | 18 | if __name__ == "__main__": 19 | parser = argparse.ArgumentParser(description='matrix_demo arguments', 20 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 21 | 22 | parser.add_argument('--cascaded', '-n', type=int, default=5, help='Number of cascaded MAX7219 LED matrices') 23 | parser.add_argument('--block-orientation', type=int, default=0, choices=[0, 90, -90], help='Corrects block orientation when wired vertically') 24 | 25 | args = parser.parse_args() 26 | 27 | while True: 28 | 29 | currentLoop = currentLoop + 1 30 | 31 | Tv = str(currentLoop) 32 | Tv = Tv.rjust(5, " ") 33 | 34 | with canvas(device) as draw: 35 | text(draw, (0, 0), Tv, fill="white") 36 | 37 | print(Tv) 38 | time.sleep(1) 39 | 40 | if currentLoop >= 99999: 41 | currentLoop = 0 42 | -------------------------------------------------------------------------------- /examples/larson_hue.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2017-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | # 6 | # Based on https://github.com/pimoroni/blinkt/blob/master/examples/larson_hue.py 7 | 8 | import math 9 | import time 10 | import colorsys 11 | 12 | from luma.led_matrix.device import apa102 13 | from luma.core.render import canvas 14 | 15 | device = apa102(width=8, height=1) 16 | 17 | FALLOFF = 1.9 18 | SCAN_SPEED = 4 19 | 20 | 21 | def main(): 22 | 23 | start_time = time.time() 24 | 25 | while True: 26 | delta = (time.time() - start_time) 27 | 28 | # Offset is a sine wave derived from the time delta 29 | # we use this to animate both the hue and larson scan 30 | # so they are kept in sync with each other 31 | offset = (math.sin(delta * SCAN_SPEED) + 1) / 2 32 | 33 | # Use offset to pick the right colour from the hue wheel 34 | hue = int(round(offset * 360)) 35 | 36 | # Now we generate a value from 0 to 7 37 | offset = int(round(offset * 7)) 38 | 39 | with canvas(device) as draw: 40 | for x in range(8): 41 | sat = 1.0 42 | 43 | val = 7 - (abs(offset - x) * FALLOFF) 44 | val /= 7.0 # Convert to 0.0 to 1.0 45 | val = max(val, 0.0) # Ditch negative values 46 | 47 | xhue = hue # Grab hue for this pixel 48 | xhue += (1 - val) * 10 # Use the val offset to give a slight colour trail variation 49 | xhue %= 360 # Clamp to 0-359 50 | xhue /= 360.0 # Convert to 0.0 to 1.0 51 | 52 | r, g, b = [int(c * 255) for c in colorsys.hsv_to_rgb(xhue, sat, val)] 53 | 54 | draw.point((x, 0), fill=(r, g, b, int(val * 256))) 55 | 56 | time.sleep(0.001) 57 | 58 | 59 | if __name__ == "__main__": 60 | try: 61 | main() 62 | except KeyboardInterrupt: 63 | pass 64 | -------------------------------------------------------------------------------- /examples/matrix_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2017-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | import re 7 | import time 8 | import argparse 9 | 10 | from luma.led_matrix.device import max7219 11 | from luma.core.interface.serial import spi, noop 12 | from luma.core.render import canvas 13 | from luma.core.virtual import viewport 14 | from luma.core.legacy import text, show_message 15 | from luma.core.legacy.font import proportional, CP437_FONT, TINY_FONT, SINCLAIR_FONT, LCD_FONT 16 | 17 | 18 | def demo(n, block_orientation, rotate, inreverse): 19 | # create matrix device 20 | serial = spi(port=0, device=0, gpio=noop()) 21 | device = max7219(serial, cascaded=n or 1, block_orientation=block_orientation, 22 | rotate=rotate or 0, blocks_arranged_in_reverse_order=inreverse) 23 | print("Created device") 24 | 25 | # start demo 26 | msg = "MAX7219 LED Matrix Demo" 27 | print(msg) 28 | show_message(device, msg, fill="white", font=proportional(CP437_FONT)) 29 | time.sleep(1) 30 | 31 | msg = "Fast scrolling: Lorem ipsum dolor sit amet, consectetur adipiscing\ 32 | elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut\ 33 | enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut\ 34 | aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\ 35 | voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint\ 36 | occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit\ 37 | anim id est laborum." 38 | msg = re.sub(" +", " ", msg) 39 | print(msg) 40 | show_message(device, msg, fill="white", font=proportional(LCD_FONT), scroll_delay=0) 41 | 42 | msg = "Slow scrolling: The quick brown fox jumps over the lazy dog" 43 | print(msg) 44 | show_message(device, msg, fill="white", font=proportional(LCD_FONT), scroll_delay=0.1) 45 | 46 | print("Vertical scrolling") 47 | words = [ 48 | "Victor", "Echo", "Romeo", "Tango", "India", "Charlie", "Alpha", 49 | "Lima", " ", "Sierra", "Charlie", "Romeo", "Oscar", "Lima", "Lima", 50 | "India", "November", "Golf", " " 51 | ] 52 | 53 | virtual = viewport(device, width=device.width, height=len(words) * 8) 54 | with canvas(virtual) as draw: 55 | for i, word in enumerate(words): 56 | text(draw, (0, i * 8), word, fill="white", font=proportional(CP437_FONT)) 57 | 58 | for i in range(virtual.height - device.height): 59 | virtual.set_position((0, i)) 60 | time.sleep(0.05) 61 | 62 | msg = "Brightness" 63 | print(msg) 64 | show_message(device, msg, fill="white") 65 | 66 | time.sleep(1) 67 | with canvas(device) as draw: 68 | text(draw, (0, 0), "A", fill="white") 69 | 70 | time.sleep(1) 71 | for _ in range(5): 72 | for intensity in range(16): 73 | device.contrast(intensity * 16) 74 | time.sleep(0.1) 75 | 76 | device.contrast(0x80) 77 | time.sleep(1) 78 | 79 | msg = "Alternative font!" 80 | print(msg) 81 | show_message(device, msg, fill="white", font=SINCLAIR_FONT) 82 | 83 | time.sleep(1) 84 | msg = "Proportional font - characters are squeezed together!" 85 | print(msg) 86 | show_message(device, msg, fill="white", font=proportional(SINCLAIR_FONT)) 87 | 88 | # http://www.squaregear.net/fonts/tiny.shtml 89 | time.sleep(1) 90 | msg = "Tiny is, I believe, the smallest possible font \ 91 | (in pixel size). It stands at a lofty four pixels \ 92 | tall (five if you count descenders), yet it still \ 93 | contains all the printable ASCII characters." 94 | msg = re.sub(" +", " ", msg) 95 | print(msg) 96 | show_message(device, msg, fill="white", font=proportional(TINY_FONT)) 97 | 98 | time.sleep(1) 99 | msg = "CP437 Characters" 100 | print(msg) 101 | show_message(device, msg) 102 | 103 | time.sleep(1) 104 | for x in range(256): 105 | with canvas(device) as draw: 106 | text(draw, (0, 0), chr(x), fill="white") 107 | time.sleep(0.1) 108 | 109 | 110 | if __name__ == "__main__": 111 | parser = argparse.ArgumentParser(description='matrix_demo arguments', 112 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 113 | 114 | parser.add_argument('--cascaded', '-n', type=int, default=1, help='Number of cascaded MAX7219 LED matrices') 115 | parser.add_argument('--block-orientation', type=int, default=0, choices=[0, 90, -90], help='Corrects block orientation when wired vertically') 116 | parser.add_argument('--rotate', type=int, default=0, choices=[0, 1, 2, 3], help='Rotate display 0=0°, 1=90°, 2=180°, 3=270°') 117 | parser.add_argument('--reverse-order', type=bool, default=False, help='Set to true if blocks are in reverse order') 118 | 119 | args = parser.parse_args() 120 | 121 | try: 122 | demo(args.cascaded, args.block_orientation, args.rotate, args.reverse_order) 123 | except KeyboardInterrupt: 124 | pass 125 | -------------------------------------------------------------------------------- /examples/neopixel_crawl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2017-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | import time 7 | 8 | from luma.led_matrix.device import neopixel 9 | from luma.core.render import canvas 10 | 11 | device = neopixel(cascaded=32) 12 | 13 | for i in range(device.cascaded): 14 | with canvas(device) as draw: 15 | draw.point((i, 0), fill="green") 16 | time.sleep(0.5) 17 | -------------------------------------------------------------------------------- /examples/neopixel_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2017-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | # Portions of this script were adapted from: 7 | # https://github.com/pimoroni/unicorn-hat/blob/master/examples/demo.py 8 | 9 | import math 10 | import time 11 | import colorsys 12 | 13 | from luma.led_matrix.device import neopixel 14 | from luma.core.render import canvas 15 | from luma.core.legacy import text, show_message 16 | from luma.core.legacy.font import proportional, TINY_FONT 17 | 18 | # create matrix device 19 | device = neopixel(width=8, height=4) 20 | 21 | 22 | # twisty swirly goodness 23 | def swirl(x, y, step): 24 | x -= (device.width / 2) 25 | y -= (device.height / 2) 26 | 27 | dist = math.sqrt(pow(x, 2) + pow(y, 2)) / 2.0 28 | angle = (step / 10.0) + (dist * 1.5) 29 | s = math.sin(angle) 30 | c = math.cos(angle) 31 | 32 | xs = x * c - y * s 33 | ys = x * s + y * c 34 | 35 | r = abs(xs + ys) 36 | r = r * 64.0 37 | r -= 20 38 | 39 | return (r, r + (s * 130), r + (c * 130)) 40 | 41 | 42 | # roto-zooming checker board 43 | def checker(x, y, step): 44 | x -= (device.width / 2) 45 | y -= (device.height / 2) 46 | 47 | angle = (step / 10.0) 48 | s = math.sin(angle) 49 | c = math.cos(angle) 50 | 51 | xs = x * c - y * s 52 | ys = x * s + y * c 53 | 54 | xs -= math.sin(step / 200.0) * 40.0 55 | ys -= math.cos(step / 200.0) * 40.0 56 | 57 | scale = step % 20 58 | scale /= 20 59 | scale = (math.sin(step / 50.0) / 8.0) + 0.25 60 | 61 | xs *= scale 62 | ys *= scale 63 | 64 | xo = abs(xs) - int(abs(xs)) 65 | yo = abs(ys) - int(abs(ys)) 66 | l = 0 if (math.floor(xs) + math.floor(ys)) % 2 else 1 if xo > .1 and yo > .1 else .5 67 | 68 | r, g, b = colorsys.hsv_to_rgb((step % 255) / 255.0, 1, l) 69 | 70 | return (r * 255, g * 255, b * 255) 71 | 72 | 73 | # weeee waaaah 74 | def blues_and_twos(x, y, step): 75 | x -= (device.width / 2) 76 | y -= (device.height / 2) 77 | 78 | # xs = (math.sin((x + step) / 10.0) / 2.0) + 1.0 79 | # ys = (math.cos((y + step) / 10.0) / 2.0) + 1.0 80 | 81 | scale = math.sin(step / 6.0) / 1.5 82 | r = math.sin((x * scale) / 1.0) + math.cos((y * scale) / 1.0) 83 | b = math.sin(x * scale / 2.0) + math.cos(y * scale / 2.0) 84 | g = r - .8 85 | g = 0 if g < 0 else g 86 | 87 | b -= r 88 | b /= 1.4 89 | 90 | return (r * 255, (b + g) * 255, g * 255) 91 | 92 | 93 | # rainbow search spotlights 94 | def rainbow_search(x, y, step): 95 | xs = math.sin((step) / 100.0) * 20.0 96 | ys = math.cos((step) / 100.0) * 20.0 97 | 98 | scale = ((math.sin(step / 60.0) + 1.0) / 5.0) + 0.2 99 | r = math.sin((x + xs) * scale) + math.cos((y + xs) * scale) 100 | g = math.sin((x + xs) * scale) + math.cos((y + ys) * scale) 101 | b = math.sin((x + ys) * scale) + math.cos((y + ys) * scale) 102 | 103 | return (r * 255, g * 255, b * 255) 104 | 105 | 106 | # zoom tunnel 107 | def tunnel(x, y, step): 108 | 109 | speed = step / 100.0 110 | x -= (device.width / 2) 111 | y -= (device.height / 2) 112 | 113 | xo = math.sin(step / 27.0) * 2 114 | yo = math.cos(step / 18.0) * 2 115 | 116 | x += xo 117 | y += yo 118 | 119 | if y == 0: 120 | if x < 0: 121 | angle = -(math.pi / 2) 122 | else: 123 | angle = (math.pi / 2) 124 | else: 125 | angle = math.atan(x / y) 126 | 127 | if y > 0: 128 | angle += math.pi 129 | 130 | angle /= 2 * math.pi # convert angle to 0...1 range 131 | 132 | shade = math.sqrt(math.pow(x, 2) + math.pow(y, 2)) / 2.1 133 | shade = 1 if shade > 1 else shade 134 | 135 | angle += speed 136 | depth = speed + (math.sqrt(math.pow(x, 2) + math.pow(y, 2)) / 10) 137 | 138 | col1 = colorsys.hsv_to_rgb((step % 255) / 255.0, 1, .8) 139 | col2 = colorsys.hsv_to_rgb((step % 255) / 255.0, 1, .3) 140 | 141 | col = col1 if int(abs(angle * 6.0)) % 2 == 0 else col2 142 | 143 | td = .3 if int(abs(depth * 3.0)) % 2 == 0 else 0 144 | 145 | col = (col[0] + td, col[1] + td, col[2] + td) 146 | 147 | col = (col[0] * shade, col[1] * shade, col[2] * shade) 148 | 149 | return (col[0] * 255, col[1] * 255, col[2] * 255) 150 | 151 | 152 | def gfx(device): 153 | effects = [tunnel, rainbow_search, checker, swirl] 154 | 155 | step = 0 156 | while True: 157 | for i in range(500): 158 | with canvas(device) as draw: 159 | for y in range(device.height): 160 | for x in range(device.width): 161 | r, g, b = effects[0](x, y, step) 162 | if i > 400: 163 | r2, g2, b2 = effects[-1](x, y, step) 164 | 165 | ratio = (500.00 - i) / 100.0 166 | r = r * ratio + r2 * (1.0 - ratio) 167 | g = g * ratio + g2 * (1.0 - ratio) 168 | b = b * ratio + b2 * (1.0 - ratio) 169 | r = int(max(0, min(255, r))) 170 | g = int(max(0, min(255, g))) 171 | b = int(max(0, min(255, b))) 172 | draw.point((x, y), (r, g, b)) 173 | 174 | step += 1 175 | 176 | time.sleep(0.01) 177 | 178 | effect = effects.pop() 179 | effects.insert(0, effect) 180 | 181 | 182 | def main(): 183 | msg = "Neopixel WS2812 LED Matrix Demo" 184 | show_message(device, msg, y_offset=-1, fill="green", font=proportional(TINY_FONT)) 185 | time.sleep(1) 186 | 187 | with canvas(device) as draw: 188 | text(draw, (0, -1), txt="A", fill="red", font=TINY_FONT) 189 | text(draw, (4, -1), txt="T", fill="green", font=TINY_FONT) 190 | 191 | time.sleep(1) 192 | 193 | with canvas(device) as draw: 194 | draw.line((0, 0, 0, device.height), fill="red") 195 | draw.line((1, 0, 1, device.height), fill="orange") 196 | draw.line((2, 0, 2, device.height), fill="yellow") 197 | draw.line((3, 0, 3, device.height), fill="green") 198 | draw.line((4, 0, 4, device.height), fill="blue") 199 | draw.line((5, 0, 5, device.height), fill="indigo") 200 | draw.line((6, 0, 6, device.height), fill="violet") 201 | draw.line((7, 0, 7, device.height), fill="white") 202 | 203 | time.sleep(4) 204 | 205 | for _ in range(5): 206 | for intensity in range(16): 207 | device.contrast(intensity * 16) 208 | time.sleep(0.1) 209 | 210 | device.contrast(0x80) 211 | time.sleep(1) 212 | 213 | gfx(device) 214 | 215 | 216 | if __name__ == "__main__": 217 | try: 218 | main() 219 | except KeyboardInterrupt: 220 | pass 221 | -------------------------------------------------------------------------------- /examples/neosegment_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2017-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | import time 7 | import random 8 | import colorsys 9 | from luma.led_matrix.device import neosegment 10 | from neopixel_demo import gfx 11 | 12 | 13 | def rainbow(n=1000, saturation=1, value=1): 14 | """ 15 | A generator that yields 'n' hues from the rainbow in the hex format #RRGGBB. 16 | By default the saturation and value (from HSV) are both set to 1. 17 | """ 18 | for i in range(n): 19 | hue = i / float(n) 20 | color = [int(x * 255) for x in colorsys.hsv_to_rgb(hue, saturation, value)] 21 | yield ("#%02x%02x%02x" % tuple(color)).upper() 22 | 23 | 24 | def main(): 25 | neoseg = neosegment(width=6) 26 | neoseg.text = "NEOSEG" 27 | time.sleep(1) 28 | neoseg.color[0] = "yellow" 29 | time.sleep(1) 30 | neoseg.color[3:5] = ["blue", "orange"] 31 | time.sleep(1) 32 | neoseg.color = "white" 33 | time.sleep(1) 34 | 35 | for _ in range(10): 36 | neoseg.device.hide() 37 | time.sleep(0.1) 38 | neoseg.device.show() 39 | time.sleep(0.1) 40 | 41 | time.sleep(1) 42 | 43 | for color in rainbow(200): 44 | neoseg.color = color 45 | time.sleep(0.01) 46 | 47 | colors = list(rainbow(neoseg.device.width)) 48 | for _ in range(50): 49 | random.shuffle(colors) 50 | neoseg.color = colors 51 | time.sleep(0.1) 52 | 53 | neoseg.color = "white" 54 | time.sleep(3) 55 | 56 | for _ in range(3): 57 | for intensity in range(16): 58 | neoseg.device.contrast((15 - intensity) * 16) 59 | time.sleep(0.1) 60 | 61 | for intensity in range(16): 62 | neoseg.device.contrast(intensity * 16) 63 | time.sleep(0.1) 64 | 65 | neoseg.text = "" 66 | neoseg.device.contrast(0x80) 67 | time.sleep(1) 68 | 69 | neoseg.text = "rgb" 70 | time.sleep(1) 71 | neoseg.color[0] = "red" 72 | time.sleep(1) 73 | neoseg.color[1] = "green" 74 | time.sleep(1) 75 | neoseg.color[2] = "blue" 76 | time.sleep(5) 77 | 78 | for _ in range(3): 79 | for intensity in range(16): 80 | neoseg.device.contrast(intensity * 16) 81 | time.sleep(0.1) 82 | 83 | neoseg.text = "" 84 | neoseg.device.contrast(0x80) 85 | time.sleep(1) 86 | 87 | gfx(neoseg.device) 88 | 89 | 90 | if __name__ == "__main__": 91 | try: 92 | main() 93 | except KeyboardInterrupt: 94 | pass 95 | -------------------------------------------------------------------------------- /examples/sevensegment_demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2017-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | """ 7 | Example for seven segment displays. 8 | """ 9 | 10 | import time 11 | from datetime import datetime 12 | 13 | from luma.led_matrix.device import max7219 14 | from luma.core.interface.serial import spi, noop 15 | from luma.core.virtual import viewport, sevensegment 16 | 17 | 18 | def date(seg): 19 | """ 20 | Display current date on device. 21 | """ 22 | now = datetime.now() 23 | seg.text = now.strftime("%y-%m-%d") 24 | 25 | 26 | def clock(seg, seconds): 27 | """ 28 | Display current time on device. 29 | """ 30 | interval = 0.5 31 | for i in range(int(seconds / interval)): 32 | now = datetime.now() 33 | seg.text = now.strftime("%H-%M-%S") 34 | 35 | # calculate blinking dot 36 | if i % 2 == 0: 37 | seg.text = now.strftime("%H-%M-%S") 38 | else: 39 | seg.text = now.strftime("%H %M %S") 40 | 41 | time.sleep(interval) 42 | 43 | 44 | def show_message_vp(device, msg, delay=0.1): 45 | # Implemented with virtual viewport 46 | width = device.width 47 | padding = " " * width 48 | msg = padding + msg + padding 49 | n = len(msg) 50 | 51 | virtual = viewport(device, width=n, height=8) 52 | sevensegment(virtual).text = msg 53 | for i in reversed(list(range(n - width))): 54 | virtual.set_position((i, 0)) 55 | time.sleep(delay) 56 | 57 | 58 | def show_message_alt(seg, msg, delay=0.1): 59 | # Does same as above but does string slicing itself 60 | width = seg.device.width 61 | padding = " " * width 62 | msg = padding + msg + padding 63 | 64 | for i in range(len(msg)): 65 | seg.text = msg[i:i + width] 66 | time.sleep(delay) 67 | 68 | 69 | def main(): 70 | # create seven segment device 71 | serial = spi(port=0, device=0, gpio=noop()) 72 | device = max7219(serial, cascaded=1) 73 | seg = sevensegment(device) 74 | 75 | print('Simple text...') 76 | for _ in range(8): 77 | seg.text = "HELLO" 78 | time.sleep(0.6) 79 | seg.text = " GOODBYE" 80 | time.sleep(0.6) 81 | 82 | # Digit slicing 83 | print("Digit slicing") 84 | seg.text = "_" * seg.device.width 85 | time.sleep(1.0) 86 | 87 | for i, ch in enumerate([9, 8, 7, 6, 5, 4, 3, 2]): 88 | seg.text[i] = str(ch) 89 | time.sleep(0.6) 90 | 91 | for i in range(len(seg.text)): 92 | del seg.text[0] 93 | time.sleep(0.6) 94 | 95 | # Scrolling Alphabet Text 96 | print('Scrolling alphabet text...') 97 | show_message_vp(device, "HELLO EVERYONE!") 98 | show_message_vp(device, "PI is 3.14159 ... ") 99 | show_message_vp(device, "IP is 127.0.0.1 ... ") 100 | show_message_alt(seg, "0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ") 101 | 102 | # Digit futzing 103 | date(seg) 104 | time.sleep(5) 105 | clock(seg, seconds=10) 106 | 107 | # Brightness 108 | print('Brightness...') 109 | for x in range(5): 110 | for intensity in range(16): 111 | seg.device.contrast(intensity * 16) 112 | time.sleep(0.1) 113 | device.contrast(0x7F) 114 | 115 | 116 | if __name__ == '__main__': 117 | main() 118 | -------------------------------------------------------------------------------- /examples/silly_clock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import time 3 | from datetime import datetime 4 | 5 | from luma.led_matrix.device import max7219 6 | from luma.core.interface.serial import spi, noop 7 | from luma.core.render import canvas 8 | from luma.core.legacy import text, show_message 9 | from luma.core.legacy.font import proportional, CP437_FONT, TINY_FONT 10 | 11 | 12 | def minute_change(device): 13 | '''When we reach a minute change, animate it.''' 14 | hours = datetime.now().strftime('%H') 15 | minutes = datetime.now().strftime('%M') 16 | 17 | def helper(current_y): 18 | with canvas(device) as draw: 19 | text(draw, (0, 1), hours, fill="white", font=proportional(CP437_FONT)) 20 | text(draw, (15, 1), ":", fill="white", font=proportional(TINY_FONT)) 21 | text(draw, (17, current_y), minutes, fill="white", font=proportional(CP437_FONT)) 22 | time.sleep(0.1) 23 | for current_y in range(1, 9): 24 | helper(current_y) 25 | minutes = datetime.now().strftime('%M') 26 | for current_y in range(9, 1, -1): 27 | helper(current_y) 28 | 29 | 30 | def animation(device, from_y, to_y): 31 | '''Animate the whole thing, moving it into/out of the abyss.''' 32 | hourstime = datetime.now().strftime('%H') 33 | mintime = datetime.now().strftime('%M') 34 | current_y = from_y 35 | while current_y != to_y: 36 | with canvas(device) as draw: 37 | text(draw, (0, current_y), hourstime, fill="white", font=proportional(CP437_FONT)) 38 | text(draw, (15, current_y), ":", fill="white", font=proportional(TINY_FONT)) 39 | text(draw, (17, current_y), mintime, fill="white", font=proportional(CP437_FONT)) 40 | time.sleep(0.1) 41 | current_y += 1 if to_y > from_y else -1 42 | 43 | 44 | def main(): 45 | # Setup for Banggood version of 4 x 8x8 LED Matrix (https://bit.ly/2Gywazb) 46 | serial = spi(port=0, device=0, gpio=noop()) 47 | device = max7219(serial, cascaded=4, block_orientation=-90, blocks_arranged_in_reverse_order=True) 48 | device.contrast(16) 49 | 50 | # The time ascends from the abyss... 51 | animation(device, 8, 1) 52 | 53 | toggle = False # Toggle the second indicator every second 54 | while True: 55 | toggle = not toggle 56 | sec = datetime.now().second 57 | if sec == 59: 58 | # When we change minutes, animate the minute change 59 | minute_change(device) 60 | elif sec == 30: 61 | # Half-way through each minute, display the complete date/time, 62 | # animating the time display into and out of the abyss. 63 | full_msg = time.ctime() 64 | animation(device, 1, 8) 65 | show_message(device, full_msg, fill="white", font=proportional(CP437_FONT)) 66 | animation(device, 8, 1) 67 | else: 68 | # Do the following twice a second (so the seconds' indicator blips). 69 | # I'd optimize if I had to - but what's the point? 70 | # Even my Raspberry PI2 can do this at 4% of a single one of the 4 cores! 71 | hours = datetime.now().strftime('%H') 72 | minutes = datetime.now().strftime('%M') 73 | with canvas(device) as draw: 74 | text(draw, (0, 1), hours, fill="white", font=proportional(CP437_FONT)) 75 | text(draw, (15, 1), ":" if toggle else " ", fill="white", font=proportional(TINY_FONT)) 76 | text(draw, (17, 1), minutes, fill="white", font=proportional(CP437_FONT)) 77 | time.sleep(0.5) 78 | 79 | 80 | if __name__ == "__main__": 81 | main() 82 | -------------------------------------------------------------------------------- /examples/view_message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Commandline Wrapper 4 | # Thomas Wenzlaff 5 | # See LICENSE.rst for details. 6 | 7 | import time 8 | import argparse 9 | 10 | from luma.led_matrix.device import max7219 11 | from luma.core.interface.serial import spi, noop 12 | from luma.core.legacy import show_message 13 | from luma.core.legacy.font import proportional, CP437_FONT 14 | 15 | 16 | def output(n, block_orientation, rotate, inreverse, text): 17 | # create matrix device 18 | serial = spi(port=0, device=0, gpio=noop()) 19 | device = max7219(serial, cascaded=n or 1, block_orientation=block_orientation, 20 | rotate=rotate or 0, blocks_arranged_in_reverse_order=inreverse) 21 | print(text) 22 | 23 | show_message(device, text, fill="white", font=proportional(CP437_FONT), scroll_delay=0.05) 24 | time.sleep(1) 25 | 26 | 27 | if __name__ == "__main__": 28 | parser = argparse.ArgumentParser(description='view_message arguments', 29 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) 30 | 31 | parser.add_argument('--cascaded', '-n', type=int, default=1, help='Number of cascaded MAX7219 LED matrices') 32 | parser.add_argument('--block-orientation', type=int, default=0, choices=[0, 90, -90], help='Corrects block orientation when wired vertically') 33 | parser.add_argument('--rotate', type=int, default=0, choices=[0, 1, 2, 3], help='Rotate display 0=0°, 1=90°, 2=180°, 3=270°') 34 | parser.add_argument('--reverse-order', type=bool, default=False, help='Set to true if blocks are in reverse order') 35 | parser.add_argument('--text', '-t', default='>>> No text set', help='Set text message') 36 | args = parser.parse_args() 37 | 38 | try: 39 | output(args.cascaded, args.block_orientation, args.rotate, args.reverse_order, args.text) 40 | except KeyboardInterrupt: 41 | pass 42 | -------------------------------------------------------------------------------- /luma/led_matrix/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2017-2023 Richard Hull and contributors 3 | # See LICENSE.rst for details. 4 | 5 | """ 6 | Display drivers for LED Matrices & 7-segment displays (MAX7219) and 7 | RGB NeoPixels (WS2812 / APA102). 8 | """ 9 | 10 | __version__ = "1.7.1" 11 | -------------------------------------------------------------------------------- /luma/led_matrix/const.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2013-18 Richard Hull and contributors 3 | # See LICENSE.rst for details. 4 | 5 | 6 | class max7219(object): 7 | NOOP = 0x00 8 | DIGIT_0 = 0x01 9 | DECODEMODE = 0x09 10 | INTENSITY = 0x0A 11 | SCANLIMIT = 0x0B 12 | SHUTDOWN = 0x0C 13 | DISPLAYTEST = 0x0F 14 | -------------------------------------------------------------------------------- /luma/led_matrix/device.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2017-2022 Richard Hull and contributors 3 | # See LICENSE.rst for details. 4 | 5 | """ 6 | Collection of serial interfaces to LED matrix devices. 7 | """ 8 | 9 | # Example usage: 10 | # 11 | # from luma.core.interface.serial import spi, noop 12 | # from luma.core.render import canvas 13 | # from luma.led_matrix.device import max7219 14 | # 15 | # serial = spi(port=0, device=0, gpio=noop()) 16 | # device = max7219(serial, width=8, height=8) 17 | # 18 | # with canvas(device) as draw: 19 | # draw.rectangle(device.bounding_box, outline="white", fill="black") 20 | # 21 | # As soon as the with-block scope level is complete, the graphics primitives 22 | # will be flushed to the device. 23 | # 24 | # Creating a new canvas is effectively 'carte blanche': If you want to retain 25 | # an existing canvas, then make a reference like: 26 | # 27 | # c = canvas(device) 28 | # for X in ...: 29 | # with c as draw: 30 | # draw.rectangle(...) 31 | # 32 | # As before, as soon as the with block completes, the canvas buffer is flushed 33 | # to the device 34 | 35 | from math import ceil 36 | import luma.core.error 37 | import luma.led_matrix.const 38 | from luma.core.interface.serial import noop 39 | from luma.core.device import device 40 | from luma.core.render import canvas 41 | from luma.core.util import observable 42 | from luma.core.virtual import sevensegment 43 | from luma.led_matrix.segment_mapper import dot_muncher, regular 44 | 45 | 46 | __all__ = ["max7219", "ws2812", "neopixel", "neosegment", "apa102", "unicornhathd"] 47 | 48 | 49 | class max7219(device): 50 | """ 51 | Serial interface to a series of 8x8 LED matrixes daisychained together with 52 | MAX7219 chips. 53 | 54 | On creation, an initialization sequence is pumped to the display to properly 55 | configure it. Further control commands can then be called to affect the 56 | brightness and other settings. 57 | """ 58 | def __init__(self, serial_interface=None, width=8, height=8, cascaded=None, rotate=0, 59 | block_orientation=0, blocks_arranged_in_reverse_order=False, contrast=0x70, 60 | **kwargs): 61 | super(max7219, self).__init__(luma.led_matrix.const.max7219, serial_interface) 62 | 63 | # Derive (override) the width and height if a cascaded param supplied 64 | if cascaded is not None: 65 | width = cascaded * 8 66 | height = 8 67 | 68 | self.blocks_arranged_in_reverse_order = blocks_arranged_in_reverse_order 69 | self.capabilities(width, height, rotate) 70 | self.segment_mapper = dot_muncher 71 | 72 | if width <= 0 or width % 8 != 0 or height <= 0 or height % 8 != 0: 73 | raise luma.core.error.DeviceDisplayModeError( 74 | f"Unsupported display mode: {width} x {height}") 75 | 76 | assert block_orientation in [0, 90, -90, 180] 77 | self._correction_angle = block_orientation 78 | 79 | self.cascaded = cascaded or (width * height) // 64 80 | self._offsets = [(y * self._w) + x 81 | for y in range(self._h - 8, -8, -8) 82 | for x in range(self._w - 8, -8, -8)] 83 | self._rows = list(range(8)) 84 | 85 | self.data([self._const.SCANLIMIT, 7] * self.cascaded) 86 | self.data([self._const.DECODEMODE, 0] * self.cascaded) 87 | self.data([self._const.DISPLAYTEST, 0] * self.cascaded) 88 | 89 | self.contrast(contrast) 90 | self.clear() 91 | self.show() 92 | 93 | def preprocess(self, image): 94 | """ 95 | Performs the inherited behviour (if any), and if the LED matrix 96 | orientation is declared to need correction, each 8x8 block of pixels 97 | is rotated 90° clockwise or counter-clockwise. 98 | """ 99 | image = super(max7219, self).preprocess(image) 100 | 101 | if self._correction_angle != 0: 102 | image = image.copy() 103 | for y in range(0, self._h, 8): 104 | for x in range(0, self._w, 8): 105 | box = (x, y, x + 8, y + 8) 106 | rotated_block = image.crop(box).rotate(self._correction_angle) 107 | image.paste(rotated_block, box) 108 | if self.blocks_arranged_in_reverse_order: 109 | old_image = image.copy() 110 | for y in range(8): 111 | for x in range(8): 112 | for i in range(self.cascaded): 113 | image.putpixel((8 * (self.cascaded - 1) - i * 8 + x, y), old_image.getpixel((i * 8 + x, y))) 114 | 115 | return image 116 | 117 | def display(self, image): 118 | """ 119 | Takes a 1-bit :py:mod:`PIL.Image` and dumps it to the LED matrix display 120 | via the MAX7219 serializers. 121 | """ 122 | assert image.mode == self.mode 123 | assert image.size == self.size 124 | 125 | image = self.preprocess(image) 126 | 127 | i = 0 128 | d0 = self._const.DIGIT_0 129 | step = 2 * self.cascaded 130 | offsets = self._offsets 131 | rows = self._rows 132 | 133 | buf = bytearray(8 * step) 134 | pix = list(image.getdata()) 135 | 136 | for digit in range(8): 137 | for daisychained_device in offsets: 138 | byte = 0 139 | idx = daisychained_device + digit 140 | for y in rows: 141 | if pix[idx] > 0: 142 | byte |= 1 << y 143 | idx += self._w 144 | 145 | buf[i] = digit + d0 146 | buf[i + 1] = byte 147 | i += 2 148 | 149 | buf = list(buf) 150 | for i in range(0, len(buf), step): 151 | self.data(buf[i:i + step]) 152 | 153 | def contrast(self, value): 154 | """ 155 | Sets the LED intensity to the desired level, in the range 0-255. 156 | 157 | :param level: Desired contrast level in the range of 0-255. 158 | :type level: int 159 | """ 160 | assert 0x00 <= value <= 0xFF 161 | self.data([self._const.INTENSITY, value >> 4] * self.cascaded) 162 | 163 | def show(self): 164 | """ 165 | Sets the display mode ON, waking the device out of a prior 166 | low-power sleep mode. 167 | """ 168 | self.data([self._const.SHUTDOWN, 1] * self.cascaded) 169 | 170 | def hide(self): 171 | """ 172 | Switches the display mode OFF, putting the device in low-power 173 | sleep mode. 174 | """ 175 | self.data([self._const.SHUTDOWN, 0] * self.cascaded) 176 | 177 | 178 | class ws2812(device): 179 | """ 180 | Serial interface to a series of RGB neopixels daisy-chained together with 181 | WS281x chips. 182 | 183 | On creation, the array is initialized with the correct number of cascaded 184 | devices. Further control commands can then be called to affect the 185 | brightness and other settings. 186 | 187 | :param dma_interface: The WS2812 interface to write to (usually omit this 188 | parameter and it will default to the correct value - it is only needed 189 | for testing whereby a mock implementation is supplied). 190 | :param width: The number of pixels laid out horizontally. 191 | :type width: int 192 | :param height: The number of pixels laid out vertically. 193 | :type width: int 194 | :param cascaded: The number of pixels in a single strip - if supplied, this 195 | will override ``width`` and ``height``. 196 | :type cascaded: int 197 | :param rotate: Whether the device dimensions should be rotated in-situ: 198 | A value of: 0=0°, 1=90°, 2=180°, 3=270°. If not supplied, zero is 199 | assumed. 200 | :type rotate: int 201 | :param mapping: An (optional) array of integer values that translate the 202 | pixel to physical offsets. If supplied, should be the same size as 203 | ``width * height``. 204 | :type mapping: int[] 205 | 206 | .. versionadded:: 0.4.0 207 | """ 208 | def __init__(self, dma_interface=None, width=8, height=4, cascaded=None, 209 | rotate=0, mapping=None, **kwargs): 210 | super(ws2812, self).__init__(const=None, serial_interface=noop) 211 | 212 | # Derive (override) the width and height if a cascaded param supplied 213 | if cascaded is not None: 214 | width = cascaded 215 | height = 1 216 | 217 | self.cascaded = width * height 218 | self.capabilities(width, height, rotate, mode="RGB") 219 | self._mapping = list(mapping or range(self.cascaded)) 220 | assert self.cascaded == len(self._mapping) 221 | self._contrast = None 222 | self._prev_contrast = 0x70 223 | 224 | ws = self._ws = dma_interface or self.__ws281x__() 225 | 226 | # Create ws2811_t structure and fill in parameters. 227 | self._leds = ws.new_ws2811_t() 228 | 229 | pin = 18 230 | channel = 0 231 | dma = 10 232 | freq_hz = 800000 233 | brightness = 255 234 | strip_type = ws.WS2811_STRIP_GRB 235 | invert = False 236 | 237 | # Initialize the channels to zero 238 | for channum in range(2): 239 | chan = ws.ws2811_channel_get(self._leds, channum) 240 | ws.ws2811_channel_t_count_set(chan, 0) 241 | ws.ws2811_channel_t_gpionum_set(chan, 0) 242 | ws.ws2811_channel_t_invert_set(chan, 0) 243 | ws.ws2811_channel_t_brightness_set(chan, 0) 244 | 245 | # Initialize the channel in use 246 | self._channel = ws.ws2811_channel_get(self._leds, channel) 247 | ws.ws2811_channel_t_count_set(self._channel, self.cascaded) 248 | ws.ws2811_channel_t_gpionum_set(self._channel, pin) 249 | ws.ws2811_channel_t_invert_set(self._channel, 0 if not invert else 1) 250 | ws.ws2811_channel_t_brightness_set(self._channel, brightness) 251 | ws.ws2811_channel_t_strip_type_set(self._channel, strip_type) 252 | 253 | # Initialize the controller 254 | ws.ws2811_t_freq_set(self._leds, freq_hz) 255 | ws.ws2811_t_dmanum_set(self._leds, dma) 256 | 257 | resp = ws.ws2811_init(self._leds) 258 | if resp != 0: 259 | raise RuntimeError(f'ws2811_init failed with code {resp}') 260 | 261 | self.clear() 262 | self.show() 263 | 264 | def __ws281x__(self): 265 | import _rpi_ws281x 266 | return _rpi_ws281x 267 | 268 | def display(self, image): 269 | """ 270 | Takes a 24-bit RGB :py:mod:`PIL.Image` and dumps it to the daisy-chained 271 | WS2812 neopixels. 272 | """ 273 | assert image.mode == self.mode 274 | assert image.size == self.size 275 | 276 | ws = self._ws 277 | m = self._mapping 278 | for idx, (red, green, blue) in enumerate(image.getdata()): 279 | color = (red << 16) | (green << 8) | blue 280 | ws.ws2811_led_set(self._channel, m[idx], color) 281 | 282 | self._flush() 283 | 284 | def show(self): 285 | """ 286 | Simulates switching the display mode ON; this is achieved by restoring 287 | the contrast to the level prior to the last time hide() was called. 288 | """ 289 | if self._prev_contrast is not None: 290 | self.contrast(self._prev_contrast) 291 | self._prev_contrast = None 292 | 293 | def hide(self): 294 | """ 295 | Simulates switching the display mode OFF; this is achieved by setting 296 | the contrast level to zero. 297 | """ 298 | if self._prev_contrast is None: 299 | self._prev_contrast = self._contrast 300 | self.contrast(0x00) 301 | 302 | def contrast(self, value): 303 | """ 304 | Sets the LED intensity to the desired level, in the range 0-255. 305 | 306 | :param level: Desired contrast level in the range of 0-255. 307 | :type level: int 308 | """ 309 | assert 0x00 <= value <= 0xFF 310 | self._contrast = value 311 | self._ws.ws2811_channel_t_brightness_set(self._channel, value) 312 | self._flush() 313 | 314 | def _flush(self): 315 | resp = self._ws.ws2811_render(self._leds) 316 | if resp != 0: 317 | raise RuntimeError(f'ws2811_render failed with code {resp}') 318 | 319 | def __del__(self): 320 | # Required because Python will complain about memory leaks 321 | # However there's no guarantee that "ws" will even be set 322 | # when the __del__ method for this class is reached. 323 | if self._ws is not None: 324 | self.cleanup() 325 | 326 | def cleanup(self): 327 | """ 328 | Attempt to reset the device & switching it off prior to exiting the 329 | python process. 330 | """ 331 | self.hide() 332 | self.clear() 333 | 334 | if self._leds is not None: 335 | self._ws.ws2811_fini(self._leds) 336 | self._ws.delete_ws2811_t(self._leds) 337 | self._leds = None 338 | self._channel = None 339 | 340 | 341 | # Alias for ws2812 342 | neopixel = ws2812 343 | 344 | # 8x8 Unicorn HAT has a 'snake-like' layout, so this translation 345 | # mapper linearizes that arrangement into a 'scan-like' layout. 346 | UNICORN_HAT = [ 347 | 7, 6, 5, 4, 3, 2, 1, 0, 348 | 8, 9, 10, 11, 12, 13, 14, 15, 349 | 23, 22, 21, 20, 19, 18, 17, 16, 350 | 24, 25, 26, 27, 28, 29, 30, 31, 351 | 39, 38, 37, 36, 35, 34, 33, 32, 352 | 40, 41, 42, 43, 44, 45, 46, 47, 353 | 55, 54, 53, 52, 51, 50, 49, 48, 354 | 56, 57, 58, 59, 60, 61, 62, 63 355 | ] 356 | 357 | 358 | class apa102(device): 359 | """ 360 | Serial interface to a series of 'next-gen' RGB DotStar daisy-chained 361 | together with APA102 chips. 362 | 363 | On creation, the array is initialized with the correct number of cascaded 364 | devices. Further control commands can then be called to affect the brightness 365 | and other settings. 366 | 367 | Note that the brightness of individual pixels can be set by altering the 368 | alpha channel of the RGBA image that is being displayed. 369 | 370 | :param serial_interface: The serial interface to write to (usually omit this 371 | parameter and it will default to the correct value - it is only needed 372 | for testing whereby a mock implementation is supplied). 373 | :param width: The number of pixels laid out horizontally. 374 | :type width: int 375 | :param height: The number of pixels laid out vertically. 376 | :type width: int 377 | :param cascaded: The number of pixels in a single strip - if supplied, this 378 | will override ``width`` and ``height``. 379 | :type cascaded: int 380 | :param rotate: Whether the device dimensions should be rotated in-situ: 381 | A value of: 0=0°, 1=90°, 2=180°, 3=270°. If not supplied, zero is 382 | assumed. 383 | :type rotate: int 384 | :param mapping: An (optional) array of integer values that translate the 385 | pixel to physical offsets. If supplied, should be the same size as 386 | ``width * height``. 387 | :type mapping: int[] 388 | 389 | .. versionadded:: 0.9.0 390 | """ 391 | def __init__(self, serial_interface=None, width=8, height=1, cascaded=None, 392 | rotate=0, mapping=None, **kwargs): 393 | super(apa102, self).__init__(luma.core.const.common, serial_interface or self.__bitbang__()) 394 | 395 | # Derive (override) the width and height if a cascaded param supplied 396 | if cascaded is not None: 397 | width = cascaded 398 | height = 1 399 | 400 | self.cascaded = width * height 401 | self.capabilities(width, height, rotate, mode="RGBA") 402 | self._mapping = list(mapping or range(self.cascaded)) 403 | assert self.cascaded == len(self._mapping) 404 | self._last_image = None 405 | 406 | self.contrast(0x70) 407 | self.clear() 408 | self.show() 409 | 410 | def __bitbang__(self): 411 | from luma.core.interface.serial import bitbang 412 | return bitbang(SCLK=24, SDA=23) 413 | 414 | def display(self, image): 415 | """ 416 | Takes a 32-bit RGBA :py:mod:`PIL.Image` and dumps it to the daisy-chained 417 | APA102 neopixels. If a pixel is not fully opaque, the alpha channel 418 | value is used to set the brightness of the respective RGB LED. 419 | """ 420 | assert image.mode == self.mode 421 | assert image.size == self.size 422 | self._last_image = image.copy() 423 | 424 | # Send 32 zero-bits to reset, then pixel values then n/2 zero-bits at end 425 | sz = image.width * image.height * 4 426 | buf = bytearray(4 + sz + ceil(image.width * image.height / 8 / 2)) 427 | 428 | m = self._mapping 429 | for idx, (r, g, b, a) in enumerate(image.getdata()): 430 | offset = 4 + m[idx] * 4 431 | brightness = (a >> 4) if a != 0xFF else self._brightness 432 | buf[offset] = (0xE0 | brightness) 433 | buf[offset + 1] = b 434 | buf[offset + 2] = g 435 | buf[offset + 3] = r 436 | 437 | self._serial_interface.data(list(buf)) 438 | 439 | def show(self): 440 | """ 441 | Not supported 442 | """ 443 | pass 444 | 445 | def hide(self): 446 | """ 447 | Not supported 448 | """ 449 | pass 450 | 451 | def contrast(self, value): 452 | """ 453 | Sets the LED intensity to the desired level, in the range 0-255. 454 | 455 | :param level: Desired contrast level in the range of 0-255. 456 | :type level: int 457 | """ 458 | assert 0x00 <= value <= 0xFF 459 | self._brightness = value >> 4 460 | if self._last_image is not None: 461 | self.display(self._last_image) 462 | 463 | 464 | class neosegment(sevensegment): 465 | """ 466 | Extends the :py:class:`~luma.core.virtual.sevensegment` class specifically 467 | for @msurguy's modular NeoSegments. It uses the same underlying render 468 | techniques as the base class, but provides additional functionality to be 469 | able to adddress individual characters colors. 470 | 471 | :param width: The number of 7-segment elements that are cascaded. 472 | :type width: int 473 | :param undefined: The default character to substitute when an unrenderable 474 | character is supplied to the text property. 475 | :type undefined: char 476 | 477 | .. versionadded:: 0.11.0 478 | """ 479 | def __init__(self, width, undefined="_", **kwargs): 480 | if width <= 0 or width % 2 == 1: 481 | raise luma.core.error.DeviceDisplayModeError( 482 | f"Unsupported display mode: width={width}") 483 | 484 | height = 7 485 | mapping = [(i % width) * height + (i // width) for i in range(width * height)] 486 | self.device = kwargs.get("device") or ws2812(width=width, height=height, mapping=mapping) 487 | self.undefined = undefined 488 | self._text_buffer = "" 489 | self.color = "white" 490 | 491 | @property 492 | def color(self): 493 | return self._colors 494 | 495 | @color.setter 496 | def color(self, value): 497 | if not isinstance(value, list): 498 | value = [value] * self.device.width 499 | 500 | assert len(value) == self.device.width 501 | self._colors = observable(value, observer=self._color_chg) 502 | 503 | def _color_chg(self, color): 504 | self._flush(self.text, color) 505 | 506 | def _flush(self, text, color=None): 507 | data = bytearray(self.segment_mapper(text, notfound=self.undefined)).ljust(self.device.width, b'\0') 508 | color = color or self.color 509 | 510 | if len(data) > self.device.width: 511 | raise OverflowError( 512 | "Device's capabilities insufficient for value '{0}'".format(text)) 513 | 514 | with canvas(self.device) as draw: 515 | for x, byte in enumerate(data): 516 | for y in range(self.device.height): 517 | if byte & 0x01: 518 | draw.point((x, y), fill=color[x]) 519 | byte >>= 1 520 | 521 | def segment_mapper(self, text, notfound="_"): 522 | for char in regular(text, notfound): 523 | 524 | # Convert from std MAX7219 segment mappings 525 | a = char >> 6 & 0x01 526 | b = char >> 5 & 0x01 527 | c = char >> 4 & 0x01 528 | d = char >> 3 & 0x01 529 | e = char >> 2 & 0x01 530 | f = char >> 1 & 0x01 531 | g = char >> 0 & 0x01 532 | 533 | # To NeoSegment positions 534 | yield \ 535 | b << 6 | \ 536 | a << 5 | \ 537 | f << 4 | \ 538 | g << 3 | \ 539 | c << 2 | \ 540 | d << 1 | \ 541 | e << 0 542 | 543 | 544 | class unicornhathd(device): 545 | """ 546 | Display adapter for Pimoroni's Unicorn Hat HD - a dense 16x16 array of 547 | high intensity RGB LEDs. Since the board contains a small ARM chip to 548 | manage the LEDs, interfacing is very straightforward using SPI. This has 549 | the side-effect that the board appears not to be daisy-chainable though. 550 | However there a number of undocumented contact pads on the underside of 551 | the board which _may_ allow this behaviour. 552 | 553 | Note that the brightness of individual pixels can be set by altering the 554 | alpha channel of the RGBA image that is being displayed. 555 | 556 | :param serial_interface: The serial interface to write to. 557 | :param rotate: Whether the device dimensions should be rotated in-situ: 558 | A value of: 0=0°, 1=90°, 2=180°, 3=270°. If not supplied, zero is 559 | assumed. 560 | :type rotate: int 561 | 562 | .. versionadded:: 1.3.0 563 | """ 564 | def __init__(self, serial_interface=None, rotate=0, **kwargs): 565 | super(unicornhathd, self).__init__(luma.core.const.common, serial_interface) 566 | self.capabilities(16, 16, rotate, mode="RGBA") 567 | self._last_image = None 568 | self._prev_brightness = None 569 | self.contrast(0x70) 570 | self.clear() 571 | self.show() 572 | 573 | def display(self, image): 574 | """ 575 | Takes a 32-bit RGBA :py:mod:`PIL.Image` and dumps it to the Unicorn HAT HD. 576 | If a pixel is not fully opaque, the alpha channel value is used to set the 577 | brightness of the respective RGB LED. 578 | """ 579 | assert image.mode == self.mode 580 | assert image.size == self.size 581 | self._last_image = image.copy() 582 | 583 | # Send zeros to reset, then pixel values then zeros at end 584 | sz = image.width * image.height * 3 585 | buf = bytearray(sz) 586 | normalized_brightness = self._brightness / 255.0 587 | 588 | for idx, (r, g, b, a) in enumerate(image.getdata()): 589 | offset = idx * 3 590 | brightness = a / 255.0 if a != 255 else normalized_brightness 591 | buf[offset] = int(r * brightness) 592 | buf[offset + 1] = int(g * brightness) 593 | buf[offset + 2] = int(b * brightness) 594 | 595 | self._serial_interface.data([0x72] + list(buf)) # 0x72 == SOF ... start of frame? 596 | 597 | def show(self): 598 | """ 599 | Simulates switching the display mode ON; this is achieved by restoring 600 | the contrast to the level prior to the last time hide() was called. 601 | """ 602 | if self._prev_brightness is not None: 603 | self.contrast(self._prev_brightness) 604 | self._prev_brightness = None 605 | 606 | def hide(self): 607 | """ 608 | Simulates switching the display mode OFF; this is achieved by setting 609 | the contrast level to zero. 610 | """ 611 | if self._prev_brightness is None: 612 | self._prev_brightness = self._brightness 613 | self.contrast(0x00) 614 | 615 | def contrast(self, value): 616 | """ 617 | Sets the LED intensity to the desired level, in the range 0-255. 618 | 619 | :param level: Desired contrast level in the range of 0-255. 620 | :type level: int 621 | """ 622 | assert 0x00 <= value <= 0xFF 623 | self._brightness = value 624 | if self._last_image is not None: 625 | self.display(self._last_image) 626 | -------------------------------------------------------------------------------- /luma/led_matrix/segment_mapper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2017-18 Richard Hull and contributors 3 | # See LICENSE.rst for details. 4 | 5 | _DIGITS = { 6 | ' ': 0x00, 7 | '!': 0xa0, 8 | '"': 0x22, 9 | '#': 0x3f, 10 | '$': 0x5b, 11 | '%': 0xa5, 12 | "'": 0x02, 13 | '(': 0x4e, 14 | ')': 0x78, 15 | '*': 0x49, 16 | '+': 0x07, 17 | ',': 0x80, 18 | '-': 0x01, 19 | '.': 0x80, 20 | '/': 0x25, 21 | '0': 0x7e, 22 | '1': 0x30, 23 | '2': 0x6d, 24 | '3': 0x79, 25 | '4': 0x33, 26 | '5': 0x5b, 27 | '6': 0x5f, 28 | '7': 0x70, 29 | '8': 0x7f, 30 | '9': 0x7b, 31 | ':': 0x48, 32 | ';': 0x58, 33 | '<': 0x0d, 34 | '=': 0x09, 35 | '>': 0x19, 36 | '?': 0xe5, 37 | '@': 0x6f, 38 | 'A': 0x77, 39 | 'B': 0x7f, 40 | 'C': 0x4e, 41 | 'D': 0x7e, 42 | 'E': 0x4f, 43 | 'F': 0x47, 44 | 'G': 0x5e, 45 | 'H': 0x37, 46 | 'I': 0x30, 47 | 'J': 0x38, 48 | 'K': 0x57, 49 | 'L': 0x0e, 50 | 'M': 0x54, 51 | 'N': 0x76, 52 | 'O': 0x7e, 53 | 'P': 0x67, 54 | 'Q': 0x73, 55 | 'R': 0x46, 56 | 'S': 0x5b, 57 | 'T': 0x0f, 58 | 'U': 0x3e, 59 | 'V': 0x3e, 60 | 'W': 0x2a, 61 | 'X': 0x37, 62 | 'Y': 0x3b, 63 | 'Z': 0x6d, 64 | '[': 0x43, 65 | '\\': 0x13, 66 | ']': 0x61, 67 | '^': 0x62, 68 | '_': 0x08, 69 | '`': 0x20, 70 | 'a': 0x7d, 71 | 'b': 0x1f, 72 | 'c': 0x0d, 73 | 'd': 0x3d, 74 | 'e': 0x6f, 75 | 'f': 0x47, 76 | 'g': 0x7b, 77 | 'h': 0x17, 78 | 'i': 0x10, 79 | 'j': 0x18, 80 | 'k': 0x57, 81 | 'l': 0x06, 82 | 'm': 0x14, 83 | 'n': 0x15, 84 | 'o': 0x1d, 85 | 'p': 0x67, 86 | 'q': 0x73, 87 | 'r': 0x05, 88 | 's': 0x5b, 89 | 't': 0x0f, 90 | 'u': 0x1c, 91 | 'v': 0x1c, 92 | 'w': 0x14, 93 | 'x': 0x37, 94 | 'y': 0x3b, 95 | 'z': 0x6d, 96 | '{': 0x31, 97 | '|': 0x06, 98 | '}': 0x07, 99 | '~': 0x40, 100 | u'°': 0x63, 101 | u'\xb0': 0x63, 102 | } 103 | 104 | 105 | def regular(text, notfound="_"): 106 | undefined = _DIGITS[notfound] if notfound is not None else None 107 | for char in iter(text): 108 | digit = _DIGITS.get(char, undefined) 109 | if digit is not None: 110 | yield digit 111 | 112 | 113 | def dot_muncher(text, notfound="_"): 114 | if not text: 115 | return 116 | 117 | undefined = _DIGITS[notfound] if notfound is not None else None 118 | last = None 119 | for char in iter(text): 120 | curr = _DIGITS.get(char, undefined) 121 | 122 | if curr == 0x80: 123 | yield curr | (last or 0) 124 | elif last != 0x80 and last is not None: 125 | yield last 126 | 127 | last = curr 128 | 129 | if curr != 0x80 and curr is not None: 130 | yield curr 131 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools >= 40.6.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --timeout=10 -v -r wsx 3 | 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = luma.led_matrix 3 | version = attr: luma.led_matrix.__version__ 4 | description = A library to drive a MAX7219 LED serializer (using SPI) and WS2812 NeoPixels (using DMA) 5 | long_description = file: README.rst, CONTRIBUTING.rst, CHANGES.rst 6 | long_description_content_type = text/x-rst 7 | keywords = raspberry pi, rpi, led, max7219, matrix, seven segment, 7 segment, neopixel, neosegment, ws2812, ws281x, apa102, unicorn-phat, unicorn-hat, unicorn-hat-hd 8 | author = Richard Hull 9 | author_email = richard.hull@destructuring-bind.org 10 | url = https://github.com/rm-hull/luma.led_matrix 11 | license = MIT 12 | classifiers = 13 | License :: OSI Approved :: MIT License 14 | Development Status :: 5 - Production/Stable 15 | Intended Audience :: Education 16 | Intended Audience :: Developers 17 | Topic :: Education 18 | Topic :: System :: Hardware 19 | Topic :: System :: Hardware :: Hardware Drivers 20 | Programming Language :: Python :: 3 21 | Programming Language :: Python :: 3.8 22 | Programming Language :: Python :: 3.9 23 | Programming Language :: Python :: 3.10 24 | Programming Language :: Python :: 3.11 25 | Programming Language :: Python :: 3.12 26 | Programming Language :: Python :: 3.13 27 | 28 | [options] 29 | zip_safe = False 30 | packages = find_namespace: 31 | python_requires = >=3.8, <4 32 | install_requires = 33 | luma.core>=2.4.0 34 | rpi_ws281x; platform_machine=="armv7l" and platform_system=="Linux" 35 | tests_require = 36 | pytest 37 | pytest-cov 38 | pytest-timeout 39 | 40 | [options.packages.find] 41 | include = luma* 42 | 43 | [options.extras_require] 44 | docs = 45 | sphinx>=1.5.1 46 | sphinx-rtd-theme 47 | qa = 48 | flake8 49 | rstcheck 50 | test = 51 | pytest 52 | pytest-cov 53 | pytest-timeout 54 | 55 | [bdist_wheel] 56 | universal = 1 57 | 58 | [flake8] 59 | ignore = E126, E127, E128, E241, E402, E501, E731, E741 60 | exclude = 61 | .tox, 62 | # No need to traverse our git directory 63 | .git, 64 | .vscode, 65 | # There's no value in checking cache directories 66 | __pycache__, 67 | doc, 68 | build, 69 | dist 70 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import setuptools 5 | 6 | if __name__ == "__main__": 7 | setuptools.setup() 8 | -------------------------------------------------------------------------------- /tests/baseline_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2014-2020 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | """ 7 | Collection of datasets to prevent regression bugs from creeping in. 8 | """ 9 | 10 | import json 11 | from pathlib import Path 12 | 13 | 14 | def get_json_data(fname): 15 | """ 16 | Load JSON reference data. 17 | 18 | :param fname: Filename without extension. 19 | :type fname: str 20 | """ 21 | base_dir = Path(__file__).resolve().parent 22 | fpath = base_dir.joinpath('reference', 'data', fname + '.json') 23 | with fpath.open() as f: 24 | return json.load(f) 25 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2017-2020 Richard Hull and contributors 3 | # See LICENSE.rst for details. 4 | 5 | from pathlib import Path 6 | from unittest.mock import Mock 7 | 8 | import pytest 9 | from PIL import ImageChops 10 | 11 | import luma.core.error 12 | 13 | serial = Mock(unsafe=True) 14 | 15 | 16 | def setup_function(function): 17 | """ 18 | Called after a test finished. 19 | """ 20 | serial.reset_mock() 21 | serial.command.side_effect = None 22 | 23 | 24 | def assert_invalid_dimensions(deviceType, serial_interface, width, height): 25 | """ 26 | Assert an invalid resolution raises a 27 | :py:class:`luma.core.error.DeviceDisplayModeError`. 28 | """ 29 | with pytest.raises(luma.core.error.DeviceDisplayModeError) as ex: 30 | deviceType(serial_interface, width=width, height=height) 31 | assert f"Unsupported display mode: {width} x {height}" in str(ex.value) 32 | 33 | 34 | def get_reference_file(fname): 35 | """ 36 | Get absolute path for ``fname``. 37 | 38 | :param fname: Filename. 39 | :type fname: str or pathlib.Path 40 | :rtype: str 41 | """ 42 | return str(Path(__file__).resolve().parent.joinpath('reference', fname)) 43 | 44 | 45 | def get_reference_image(fname): 46 | """ 47 | :param fname: Filename. 48 | :type fname: str or pathlib.Path 49 | """ 50 | return get_reference_file(Path('images').joinpath(fname)) 51 | 52 | 53 | def assert_identical_image(reference, target): 54 | bbox = ImageChops.difference(reference, target).getbbox() 55 | assert bbox is None 56 | -------------------------------------------------------------------------------- /tests/reference/data/demo_unicornhathd.json: -------------------------------------------------------------------------------- 1 | [ 2 | 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 3 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 4 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 5 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 6 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 7 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 8 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 9 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 10 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 11 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 12 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 13 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 14 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 15 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 16 | 112, 112, 112, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 112, 112, 17 | 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112, 112 18 | ] 19 | -------------------------------------------------------------------------------- /tests/reference/data/demo_unicornhathd_alphablend.json: -------------------------------------------------------------------------------- 1 | [ 2 | 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 3 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 4 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 5 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 6 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 7 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 8 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 9 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 10 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 11 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 12 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 13 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 14 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 15 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 16 | 32, 16, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 16, 8, 17 | 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8, 32, 16, 8 18 | ] 19 | -------------------------------------------------------------------------------- /tests/reference/images/neosegment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rm-hull/luma.led_matrix/35b584c2eb7e8c9083099e7309c6fcc1ff524872/tests/reference/images/neosegment.png -------------------------------------------------------------------------------- /tests/test_apa102.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2014-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | from math import ceil 7 | from luma.led_matrix.device import apa102 8 | from luma.core.render import canvas 9 | 10 | from helpers import serial, setup_function # noqa: F401 11 | 12 | 13 | def start_frame(): 14 | return [0] * 4 15 | 16 | 17 | def end_frame(n): 18 | return [0] * ceil(n / 2 / 8) 19 | 20 | 21 | def test_init_cascaded(): 22 | device = apa102(serial, cascaded=7) 23 | assert device.width == 7 24 | assert device.height == 1 25 | serial.data.assert_called_with(start_frame() + [0xE0, 0, 0, 0] * 7 + end_frame(7)) 26 | 27 | 28 | def test_hide(): 29 | device = apa102(serial, cascaded=5) 30 | serial.reset_mock() 31 | device.hide() 32 | serial.data.assert_not_called() 33 | 34 | 35 | def test_show(): 36 | device = apa102(serial, cascaded=5) 37 | serial.reset_mock() 38 | device.show() 39 | serial.data.assert_not_called() 40 | 41 | 42 | def test_contrast(): 43 | device = apa102(serial, cascaded=6) 44 | with canvas(device) as draw: 45 | draw.rectangle(device.bounding_box, outline="red") 46 | serial.reset_mock() 47 | device.contrast(0x6B) 48 | serial.data.assert_called_with( 49 | start_frame() + [0xE6, 0, 0, 0xFF] * 6 + end_frame(6) 50 | ) 51 | 52 | 53 | def test_display(): 54 | device = apa102(serial, width=4, height=1) 55 | serial.reset_mock() 56 | 57 | with canvas(device) as draw: 58 | draw.rectangle(device.bounding_box, outline=(0x11, 0x22, 0x33, 0x44)) 59 | 60 | serial.data.assert_called_with( 61 | start_frame() + [0xE4, 0x33, 0x22, 0x11] * 4 + end_frame(4) 62 | ) 63 | -------------------------------------------------------------------------------- /tests/test_max7219.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2014-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | import pytest 7 | 8 | from luma.led_matrix.device import max7219 9 | from luma.core.render import canvas 10 | 11 | from helpers import setup_function, serial, assert_invalid_dimensions # noqa: F401 12 | from unittest.mock import call 13 | 14 | 15 | def test_init_cascaded(): 16 | device = max7219(serial, cascaded=4) 17 | assert device.width == 32 18 | assert device.height == 8 19 | 20 | 21 | def test_init_reversed(): 22 | device = max7219(serial, cascaded=4, blocks_arranged_in_reverse_order=True) 23 | assert device.blocks_arranged_in_reverse_order is True 24 | 25 | 26 | def test_init_8x8(): 27 | device = max7219(serial) 28 | assert device.cascaded == 1 29 | serial.data.assert_has_calls([ 30 | call([11, 7]), 31 | call([9, 0]), 32 | call([15, 0]), 33 | call([10, 7]), 34 | call([1, 0]), 35 | call([2, 0]), 36 | call([3, 0]), 37 | call([4, 0]), 38 | call([5, 0]), 39 | call([6, 0]), 40 | call([7, 0]), 41 | call([8, 0]), 42 | call([12, 1]) 43 | ]) 44 | 45 | 46 | def test_init_16x8(): 47 | device = max7219(serial, width=16, height=8) 48 | assert device.cascaded == 2 49 | serial.data.assert_has_calls([ 50 | call([11, 7, 11, 7]), 51 | call([9, 0, 9, 0]), 52 | call([15, 0, 15, 0]), 53 | call([10, 7, 10, 7]), 54 | call([1, 0, 1, 0]), 55 | call([2, 0, 2, 0]), 56 | call([3, 0, 3, 0]), 57 | call([4, 0, 4, 0]), 58 | call([5, 0, 5, 0]), 59 | call([6, 0, 6, 0]), 60 | call([7, 0, 7, 0]), 61 | call([8, 0, 8, 0]), 62 | call([12, 1, 12, 1]) 63 | ]) 64 | 65 | 66 | def test_init_invalid_dimensions(): 67 | assert_invalid_dimensions(max7219, serial, 59, 22) 68 | 69 | 70 | def test_hide(): 71 | device = max7219(serial, cascaded=5) 72 | serial.reset_mock() 73 | device.hide() 74 | serial.data.assert_called_once_with([12, 0] * 5) 75 | 76 | 77 | def test_show(): 78 | device = max7219(serial, cascaded=3) 79 | serial.reset_mock() 80 | device.show() 81 | serial.data.assert_called_once_with([12, 1] * 3) 82 | 83 | 84 | def test_contrast(): 85 | device = max7219(serial, cascaded=6) 86 | serial.reset_mock() 87 | device.contrast(0x6B) 88 | serial.data.assert_called_once_with([10, 6] * 6) 89 | 90 | 91 | def test_display_16x8(): 92 | device = max7219(serial, cascaded=2) 93 | serial.reset_mock() 94 | 95 | with canvas(device) as draw: 96 | draw.rectangle(device.bounding_box, outline="white") 97 | 98 | serial.data.assert_has_calls([ 99 | call([1, 0x81, 1, 0xFF]), 100 | call([2, 0x81, 2, 0x81]), 101 | call([3, 0x81, 3, 0x81]), 102 | call([4, 0x81, 4, 0x81]), 103 | call([5, 0x81, 5, 0x81]), 104 | call([6, 0x81, 6, 0x81]), 105 | call([7, 0x81, 7, 0x81]), 106 | call([8, 0xFF, 8, 0x81]) 107 | ]) 108 | 109 | 110 | def test_display_16x16(): 111 | device = max7219(serial, width=16, height=16) 112 | serial.reset_mock() 113 | 114 | with canvas(device) as draw: 115 | draw.rectangle(device.bounding_box, outline="white") 116 | 117 | serial.data.assert_has_calls([ 118 | call([1, 0x80, 1, 0xFF, 1, 0x01, 1, 0xFF]), 119 | call([2, 0x80, 2, 0x80, 2, 0x01, 2, 0x01]), 120 | call([3, 0x80, 3, 0x80, 3, 0x01, 3, 0x01]), 121 | call([4, 0x80, 4, 0x80, 4, 0x01, 4, 0x01]), 122 | call([5, 0x80, 5, 0x80, 5, 0x01, 5, 0x01]), 123 | call([6, 0x80, 6, 0x80, 6, 0x01, 6, 0x01]), 124 | call([7, 0x80, 7, 0x80, 7, 0x01, 7, 0x01]), 125 | call([8, 0xFF, 8, 0x80, 8, 0xFF, 8, 0x01]) 126 | ]) 127 | 128 | 129 | def test_normal_alignment(): 130 | device = max7219(serial, cascaded=2, block_orientation=0) 131 | serial.reset_mock() 132 | 133 | with canvas(device) as draw: 134 | draw.rectangle((0, 0, 15, 3), outline="white") 135 | 136 | serial.data.assert_has_calls([ 137 | call([1, 0x09, 1, 0x0F]), 138 | call([2, 0x09, 2, 0x09]), 139 | call([3, 0x09, 3, 0x09]), 140 | call([4, 0x09, 4, 0x09]), 141 | call([5, 0x09, 5, 0x09]), 142 | call([6, 0x09, 6, 0x09]), 143 | call([7, 0x09, 7, 0x09]), 144 | call([8, 0x0F, 8, 0x09]) 145 | ]) 146 | 147 | 148 | def test_block_realignment_minus90(): 149 | device = max7219(serial, cascaded=2, block_orientation=-90) 150 | serial.reset_mock() 151 | 152 | with canvas(device) as draw: 153 | draw.rectangle((0, 0, 15, 3), outline="white") 154 | 155 | serial.data.assert_has_calls([ 156 | call([1, 0x00, 1, 0x00]), 157 | call([2, 0x00, 2, 0x00]), 158 | call([3, 0x00, 3, 0x00]), 159 | call([4, 0x00, 4, 0x00]), 160 | call([5, 0xFF, 5, 0xFF]), 161 | call([6, 0x80, 6, 0x01]), 162 | call([7, 0x80, 7, 0x01]), 163 | call([8, 0xFF, 8, 0xFF]) 164 | ]) 165 | 166 | 167 | def test_block_realignment_plus90(): 168 | device = max7219(serial, cascaded=2, block_orientation=90) 169 | serial.reset_mock() 170 | 171 | with canvas(device) as draw: 172 | draw.rectangle((0, 0, 15, 3), outline="white") 173 | 174 | serial.data.assert_has_calls([ 175 | call([1, 0xFF, 1, 0xFF]), 176 | call([2, 0x01, 2, 0x80]), 177 | call([3, 0x01, 3, 0x80]), 178 | call([4, 0xFF, 4, 0xFF]), 179 | call([5, 0x00, 5, 0x00]), 180 | call([6, 0x00, 6, 0x00]), 181 | call([7, 0x00, 7, 0x00]), 182 | call([8, 0x00, 8, 0x00]) 183 | ]) 184 | 185 | 186 | def test_block_realignment_plus180(): 187 | device = max7219(serial, cascaded=2, block_orientation=180) 188 | serial.reset_mock() 189 | 190 | with canvas(device) as draw: 191 | draw.rectangle((0, 0, 15, 3), outline="white") 192 | 193 | serial.data.assert_has_calls([ 194 | call([1, 0xF0, 1, 0x90]), 195 | call([2, 0x90, 2, 0x90]), 196 | call([3, 0x90, 3, 0x90]), 197 | call([4, 0x90, 4, 0x90]), 198 | call([5, 0x90, 5, 0x90]), 199 | call([6, 0x90, 6, 0x90]), 200 | call([7, 0x90, 7, 0x90]), 201 | call([8, 0x90, 8, 0xF0]) 202 | ]) 203 | 204 | 205 | def test_reversed_max7219(): 206 | device = max7219(serial, cascaded=4, blocks_arranged_in_reverse_order=True) 207 | serial.reset_mock() 208 | 209 | with canvas(device) as draw: 210 | draw.rectangle((0, 0, 15, 3), outline="white") 211 | 212 | serial.data.assert_has_calls([ 213 | call([1, 15, 1, 9, 1, 0, 1, 0]), 214 | call([2, 9, 2, 9, 2, 0, 2, 0]), 215 | call([3, 9, 3, 9, 3, 0, 3, 0]), 216 | call([4, 9, 4, 9, 4, 0, 4, 0]), 217 | call([5, 9, 5, 9, 5, 0, 5, 0]), 218 | call([6, 9, 6, 9, 6, 0, 6, 0]), 219 | call([7, 9, 7, 9, 7, 0, 7, 0]), 220 | call([8, 9, 8, 15, 8, 0, 8, 0]) 221 | ]) 222 | 223 | 224 | def test_unknown_block_orientation(): 225 | with pytest.raises(AssertionError): 226 | max7219(serial, cascaded=2, block_orientation="sausages") 227 | -------------------------------------------------------------------------------- /tests/test_neosegment.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2017-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | import pytest 7 | 8 | from PIL import Image 9 | 10 | from luma.led_matrix.device import neosegment 11 | from luma.core.device import dummy 12 | from luma.core.render import canvas 13 | import luma.core.error 14 | 15 | from helpers import assert_identical_image, get_reference_image 16 | 17 | 18 | def test_invalid_dimensions(): 19 | with pytest.raises(luma.core.error.DeviceDisplayModeError) as ex: 20 | neosegment(width=3, device=dummy(width=6, height=7)) 21 | assert "Unsupported display mode: width=3" in str(ex.value) 22 | 23 | 24 | def test_overflow(): 25 | with pytest.raises(OverflowError) as ex: 26 | neoseg = neosegment(width=6, device=dummy(width=6, height=7)) 27 | neoseg.text = "TooBig!" 28 | assert "Device's capabilities insufficient for value 'TooBig!'" in str(ex.value) 29 | 30 | 31 | def test_settext_nocolor(): 32 | neoseg = neosegment(width=6, device=dummy(width=6, height=7)) 33 | neoseg.text = "888888" 34 | ref = dummy(width=6, height=7) 35 | with canvas(ref) as draw: 36 | draw.rectangle(ref.bounding_box, fill="white") 37 | assert_identical_image(ref.image, neoseg.device.image) 38 | 39 | 40 | def test_settext_singlecolor(): 41 | neoseg = neosegment(width=6, device=dummy(width=6, height=7)) 42 | neoseg.text = "888888" 43 | neoseg.color = "red" 44 | ref = dummy(width=6, height=7) 45 | with canvas(ref) as draw: 46 | draw.rectangle(ref.bounding_box, fill="red") 47 | assert_identical_image(ref.image, neoseg.device.image) 48 | 49 | 50 | def test_settext_charcolor(): 51 | neoseg = neosegment(width=6, device=dummy(width=6, height=7)) 52 | neoseg.text = "888888" 53 | neoseg.color[2] = "green" 54 | ref = dummy(width=6, height=7) 55 | with canvas(ref) as draw: 56 | draw.rectangle(ref.bounding_box, fill="white") 57 | draw.rectangle([2, 0, 2, 6], fill="green") 58 | assert_identical_image(ref.image, neoseg.device.image) 59 | 60 | 61 | def test_settext_replacechars(): 62 | neoseg = neosegment(width=6, device=dummy(width=6, height=7)) 63 | neoseg.text = "888888" 64 | neoseg.text[2:4] = " " 65 | ref = dummy(width=6, height=7) 66 | with canvas(ref) as draw: 67 | draw.rectangle(ref.bounding_box, fill="white") 68 | draw.rectangle([2, 0, 3, 6], fill="black") 69 | assert_identical_image(ref.image, neoseg.device.image) 70 | 71 | 72 | def test_segment_mapper(): 73 | img_path = get_reference_image('neosegment.png') 74 | 75 | with open(img_path, 'rb') as img: 76 | reference = Image.open(img) 77 | neoseg = neosegment(width=6, device=dummy(width=6, height=7)) 78 | neoseg.color = ["red", "green", "blue", "yellow", "cyan", "magenta"] 79 | neoseg.text = "012345" 80 | assert_identical_image(reference, neoseg.device.image) 81 | 82 | 83 | def test_unknown_char(): 84 | neoseg = neosegment(width=6, device=dummy(width=6, height=7)) 85 | neoseg.text = "888888" 86 | neoseg.text[2:4] = "&\x7f" 87 | neoseg.color[2:4] = ["orange", "orange"] 88 | ref = dummy(width=6, height=7) 89 | with canvas(ref) as draw: 90 | draw.rectangle(ref.bounding_box, fill="white") 91 | draw.rectangle([2, 0, 3, 6], fill="black") 92 | draw.rectangle([2, 1, 3, 1], fill="orange") 93 | assert_identical_image(ref.image, neoseg.device.image) 94 | -------------------------------------------------------------------------------- /tests/test_segment_mapper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2014-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | 7 | from luma.core.util import mutable_string 8 | from luma.led_matrix.segment_mapper import dot_muncher, regular 9 | 10 | 11 | def test_dot_muncher_without_dots(): 12 | buf = mutable_string("Hello world") 13 | results = dot_muncher(buf, notfound='_') 14 | assert list(results) == [0x37, 0x6f, 0x06, 0x06, 0x1d, 0x00, 0x14, 0x1d, 0x05, 0x06, 0x3d] 15 | 16 | 17 | def test_dot_muncher_with_dot(): 18 | buf = mutable_string("3.14159") 19 | results = dot_muncher(buf) 20 | assert list(results) == [0x79 | 0x80, 0x30, 0x33, 0x30, 0x5b, 0x7b] 21 | 22 | 23 | def test_dot_muncher_with_dot_at_end(): 24 | buf = mutable_string(" 525920") 25 | buf[7:] = "0." 26 | print(buf) 27 | results = dot_muncher(buf) 28 | assert list(results) == [0x00, 0x00, 0x5b, 0x6d, 0x5b, 0x7b, 0x6d, 0x7e | 0x80] 29 | 30 | 31 | def test_dot_muncher_with_dot_at_start(): 32 | buf = mutable_string(".PDF") 33 | results = dot_muncher(buf) 34 | assert list(results) == [0x80, 0x67, 0x7e, 0x47] 35 | 36 | 37 | def test_dot_muncher_with_multiple_dot(): 38 | buf = mutable_string("127.0.0.1") 39 | results = dot_muncher(buf) 40 | assert list(results) == [0x30, 0x6d, 0x70 | 0x80, 0x7e | 0x80, 0x7e | 0x80, 0x30] 41 | 42 | 43 | def test_dot_muncher_with_consecutive_dot(): 44 | buf = mutable_string("No...") 45 | results = dot_muncher(buf) 46 | assert list(results) == [0x76, 0x1d | 0x80, 0x80, 0x80] 47 | 48 | 49 | def test_dot_muncher_empty_buf(): 50 | buf = mutable_string("") 51 | results = dot_muncher(buf) 52 | assert list(results) == [] 53 | 54 | 55 | def test_dot_muncher_skips_unknown(): 56 | buf = mutable_string("B&B") 57 | results = dot_muncher(buf, notfound=None) 58 | assert list(results) == [0x7f, 0x7f] 59 | 60 | 61 | def test_dot_muncher_with_notfound(): 62 | buf = mutable_string("B&B") 63 | results = dot_muncher(buf, notfound='_') 64 | assert list(results) == [0x7f, 0x08, 0x7f] 65 | 66 | 67 | def test_regular_without_dots(): 68 | buf = mutable_string("Hello world") 69 | results = regular(buf, notfound='_') 70 | assert list(results) == [0x37, 0x6f, 0x06, 0x06, 0x1d, 0x00, 0x14, 0x1d, 0x05, 0x06, 0x3d] 71 | 72 | 73 | def test_regular_with_dot(): 74 | buf = mutable_string("3.14159") 75 | results = regular(buf) 76 | assert list(results) == [0x79, 0x80, 0x30, 0x33, 0x30, 0x5b, 0x7b] 77 | 78 | 79 | def test_regular_with_multiple_dot(): 80 | buf = mutable_string("127.0.0.1") 81 | results = regular(buf) 82 | assert list(results) == [0x30, 0x6d, 0x70, 0x80, 0x7e, 0x80, 0x7e, 0x80, 0x30] 83 | 84 | 85 | def test_regular_empty_buf(): 86 | buf = mutable_string("") 87 | results = regular(buf) 88 | assert list(results) == [] 89 | 90 | 91 | def test_regular_skips_unknown(): 92 | buf = mutable_string("B&B") 93 | results = regular(buf, notfound=None) 94 | assert list(results) == [0x7f, 0x7f] 95 | 96 | 97 | def test_regular_with_notfound(): 98 | buf = mutable_string("B&B") 99 | results = regular(buf, notfound='_') 100 | assert list(results) == [0x7f, 0x08, 0x7f] 101 | 102 | 103 | def test_degrees_unicode(): 104 | buf = mutable_string(u"29.12°C") 105 | results = dot_muncher(buf) 106 | assert list(results) == [0x6d, 0x7b | 0x80, 0x30, 0x6d, 0x63, 0x4e] 107 | 108 | 109 | def test_degrees_utf8(): 110 | buf = mutable_string(u"29.12\xb0C") 111 | results = dot_muncher(buf) 112 | assert list(results) == [0x6d, 0x7b | 0x80, 0x30, 0x6d, 0x63, 0x4e] 113 | -------------------------------------------------------------------------------- /tests/test_unicornhathd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2014-19 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | from luma.led_matrix.device import unicornhathd 7 | from luma.core.render import canvas 8 | 9 | from helpers import serial 10 | from baseline_data import get_json_data 11 | 12 | 13 | def test_init(): 14 | device = unicornhathd(serial) 15 | assert device.width == 16 16 | assert device.height == 16 17 | serial.data.assert_called_once_with([0x72] + [0] * 256 * 3) 18 | 19 | 20 | def test_hide(): 21 | device = unicornhathd(serial) 22 | serial.reset_mock() 23 | device.hide() 24 | serial.data.assert_called_once_with([0x72] + [0] * 256 * 3) 25 | 26 | 27 | def test_show(): 28 | device = unicornhathd(serial) 29 | device.contrast(0xFF) 30 | with canvas(device) as draw: 31 | draw.rectangle(device.bounding_box, outline="white", fill="white") 32 | device.hide() 33 | serial.reset_mock() 34 | device.show() 35 | serial.data.assert_called_once_with([0x72] + [0xFF] * 256 * 3) 36 | 37 | 38 | def test_contrast(): 39 | device = unicornhathd(serial) 40 | with canvas(device) as draw: 41 | draw.rectangle(device.bounding_box, outline="white", fill="white") 42 | serial.reset_mock() 43 | device.contrast(0x6B) 44 | serial.data.assert_called_once_with([0x72] + [0x6B] * 256 * 3) 45 | 46 | 47 | def test_display(): 48 | device = unicornhathd(serial) 49 | serial.reset_mock() 50 | with canvas(device) as draw: 51 | draw.rectangle(device.bounding_box, outline="white") 52 | serial.data.assert_called_once_with([0x72] + get_json_data('demo_unicornhathd')) 53 | 54 | 55 | def test_alpha_blending(): 56 | device = unicornhathd(serial) 57 | serial.reset_mock() 58 | with canvas(device) as draw: 59 | draw.rectangle(device.bounding_box, outline=(255, 128, 64, 32)) 60 | serial.data.assert_called_once_with([0x72] + get_json_data('demo_unicornhathd_alphablend')) 61 | -------------------------------------------------------------------------------- /tests/test_ws2812.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright (c) 2014-18 Richard Hull and contributors 4 | # See LICENSE.rst for details. 5 | 6 | import pytest 7 | 8 | from luma.led_matrix.device import neopixel 9 | from luma.core.render import canvas 10 | 11 | from unittest.mock import Mock, call 12 | 13 | 14 | ws = Mock(unsafe=True) 15 | chan = "channel" 16 | leds = "leds" 17 | 18 | 19 | def setup_function(function): 20 | ws.reset_mock() 21 | ws.command.side_effect = None 22 | ws.ws2811_init = Mock(return_value=0) 23 | ws.ws2811_render = Mock(return_value=0) 24 | ws.ws2811_channel_get = Mock(return_value=chan) 25 | ws.ws2811_new_ws2811_t = Mock(return_value=leds) 26 | 27 | 28 | def test_init_cascaded(): 29 | device = neopixel(ws, cascaded=7) 30 | assert device.width == 7 31 | assert device.height == 1 32 | assert ws.ws2811_channel_t_count_set.called 33 | assert ws.ws2811_channel_t_gpionum_set.called 34 | assert ws.ws2811_channel_t_invert_set.called 35 | assert ws.ws2811_channel_t_brightness_set.called 36 | assert ws.ws2811_channel_t_strip_type_set.called 37 | assert ws.ws2811_t_freq_set.called 38 | assert ws.ws2811_t_dmanum_set.called 39 | assert ws.ws2811_init.called 40 | ws.ws2811_led_set.assert_has_calls([ 41 | call(chan, i, 0x000000) for i in range(7)]) 42 | assert ws.ws2811_render.called 43 | 44 | 45 | def test_init_4x8(): 46 | device = neopixel(ws) 47 | assert device.cascaded == 32 48 | assert ws.ws2811_channel_t_count_set.called 49 | assert ws.ws2811_channel_t_gpionum_set.called 50 | assert ws.ws2811_channel_t_invert_set.called 51 | assert ws.ws2811_channel_t_brightness_set.called 52 | assert ws.ws2811_channel_t_strip_type_set.called 53 | assert ws.ws2811_t_freq_set.called 54 | assert ws.ws2811_t_dmanum_set.called 55 | assert ws.ws2811_init.called 56 | ws.ws2811_led_set.assert_has_calls([ 57 | call(chan, i, 0x000000) for i in range(32)]) 58 | assert ws.ws2811_render.called 59 | 60 | 61 | def test_init_fail(): 62 | ws.reset_mock() 63 | ws.ws2811_init = Mock(return_value=-1) 64 | with pytest.raises(RuntimeError) as ex: 65 | neopixel(ws, cascaded=7) 66 | assert "ws2811_init failed with code -1" in str(ex.value) 67 | 68 | 69 | def test_clear(): 70 | device = neopixel(ws) 71 | ws.reset_mock() 72 | device.clear() 73 | ws.ws2811_led_set.assert_has_calls([ 74 | call(chan, i, 0x000000) for i in range(32)]) 75 | assert ws.ws2811_render.called 76 | 77 | 78 | def test_cleanup(): 79 | device = neopixel(ws) 80 | device.cleanup() 81 | ws.ws2811_led_set.assert_has_calls([ 82 | call(chan, i, 0x000000) for i in range(32)]) 83 | assert ws.ws2811_render.called 84 | assert ws.ws2811_fini.called 85 | assert ws.delete_ws2811_t.called 86 | assert device._leds is None 87 | assert device._channel is None 88 | 89 | 90 | def test_hide(): 91 | device = neopixel(ws, cascaded=5) 92 | ws.reset_mock() 93 | device.hide() 94 | ws.ws2811_led_set.assert_not_called() 95 | assert ws.ws2811_render.called 96 | 97 | 98 | def test_show(): 99 | device = neopixel(ws, cascaded=5) 100 | ws.reset_mock() 101 | device.hide() 102 | device.show() 103 | ws.ws2811_led_set.assert_not_called() 104 | assert ws.ws2811_render.called 105 | 106 | 107 | def test_contrast(): 108 | device = neopixel(ws, cascaded=6) 109 | ws.reset_mock() 110 | device.contrast(0x6B) 111 | ws.ws2811_channel_t_brightness_set.assert_called_once_with(chan, 0x6B) 112 | 113 | 114 | def test_display(): 115 | device = neopixel(ws, width=4, height=4) 116 | ws.reset_mock() 117 | 118 | with canvas(device) as draw: 119 | draw.rectangle(device.bounding_box, outline="red") 120 | 121 | ws.ws2811_led_set.assert_has_calls([ 122 | call(chan, 0, 0xFF0000), 123 | call(chan, 1, 0xFF0000), 124 | call(chan, 2, 0xFF0000), 125 | call(chan, 3, 0xFF0000), 126 | call(chan, 4, 0xFF0000), 127 | call(chan, 5, 0x000000), 128 | call(chan, 6, 0x000000), 129 | call(chan, 7, 0xFF0000), 130 | call(chan, 8, 0xFF0000), 131 | call(chan, 9, 0x000000), 132 | call(chan, 10, 0x000000), 133 | call(chan, 11, 0xFF0000), 134 | call(chan, 12, 0xFF0000), 135 | call(chan, 13, 0xFF0000), 136 | call(chan, 14, 0xFF0000), 137 | call(chan, 15, 0xFF0000), 138 | ]) 139 | assert ws.ws2811_render.called 140 | 141 | 142 | def test_display_fail(): 143 | device = neopixel(ws, cascaded=7) 144 | ws.reset_mock() 145 | ws.ws2811_render = Mock(return_value=-1) 146 | 147 | with pytest.raises(RuntimeError) as ex: 148 | with canvas(device) as draw: 149 | draw.rectangle(device.bounding_box, outline="red") 150 | 151 | assert "ws2811_render failed with code -1" in str(ex.value) 152 | 153 | 154 | def test_mapping(): 155 | num_pixels = 16 156 | device = neopixel(ws, cascaded=num_pixels, mapping=reversed(list(range(num_pixels)))) 157 | ws.reset_mock() 158 | 159 | with canvas(device) as draw: 160 | for i in range(device.cascaded): 161 | draw.point((i, 0), (i, 0, 0)) 162 | 163 | expected = [call(chan, num_pixels - i - 1, i << 16) for i in range(num_pixels)] 164 | ws.ws2811_led_set.assert_has_calls(expected) 165 | 166 | assert ws.ws2811_render.called 167 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2024 Richard Hull and contributors 2 | # See LICENSE.rst for details. 3 | 4 | [tox] 5 | envlist = py{38,39,310,311,312,313},qa,doc 6 | skip_missing_interpreters = True 7 | 8 | [testenv] 9 | usedevelop = true 10 | setenv = 11 | PYTHONDEVMODE=1 12 | commands = 13 | coverage erase 14 | pytest --cov=luma 15 | coverage html 16 | deps = .[test] 17 | 18 | [testenv:qa] 19 | commands = 20 | flake8 21 | rstcheck README.rst CHANGES.rst CONTRIBUTING.rst 22 | deps = .[qa] 23 | 24 | [testenv:doc] 25 | commands = 26 | sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 27 | changedir = doc 28 | deps = .[docs] 29 | --------------------------------------------------------------------------------