./0000775000175000017500000000000014223641750012145 5ustar jawn-smithjawn-smith./library/0000775000175000017500000000000014223641750013611 5ustar jawn-smithjawn-smith./library/tests/0000775000175000017500000000000014223641750014753 5ustar jawn-smithjawn-smith./library/tests/conftest.py0000664000175000017500000000212214223641750017147 0ustar jawn-smithjawn-smithimport sys import pytest import mock @pytest.fixture(scope='function', autouse=True) def cleanup(): """This fixture removes modules under test from sys.modules. This ensures that each module is fully re-imported, along with the fixtures for each test function. """ yield None del sys.modules["unicornhatmini"] @pytest.fixture(scope='function', autouse=False) def spidev(): """Mock spidev module.""" spidev = mock.MagicMock() sys.modules['spidev'] = spidev yield spidev del sys.modules['spidev'] @pytest.fixture(scope='function', autouse=False) def GPIO(): """Mock RPi.GPIO module.""" GPIO = mock.MagicMock() # Fudge for Python < 37 (possibly earlier) sys.modules['RPi'] = mock.Mock() sys.modules['RPi'].GPIO = GPIO sys.modules['RPi.GPIO'] = GPIO yield GPIO del sys.modules['RPi'] del sys.modules['RPi.GPIO'] @pytest.fixture(scope='function', autouse=False) def atexit(): """Mock atexit module.""" atexit = mock.MagicMock() sys.modules['atexit'] = atexit yield atexit del sys.modules['atexit'] ./library/tests/test_setup.py0000664000175000017500000000111314223641750017520 0ustar jawn-smithjawn-smithimport mock def test_setup(GPIO, spidev): from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() spidev.SpiDev.assert_has_calls(( mock.call(0, 0), mock.call(0, 1) ), any_order=True) GPIO.setwarnings.assert_called_once_with(False) GPIO.setmode.assert_called_once_with(GPIO.BCM) del unicornhatmini def test_shutdown(GPIO, spidev, atexit): from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() atexit.register.assert_called_once_with(unicornhatmini._exit) unicornhatmini._exit() ./library/tests/test_features.py0000664000175000017500000000461114223641750020204 0ustar jawn-smithjawn-smith"""Feature regression tests. These tests aren't particularly rigorous, but serve to guard against future regressions. """ import pytest import mock def test_set_pixel(GPIO, spidev): from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() unicornhatmini.set_pixel(0, 0, 255, 255, 255) def test_set_all(GPIO, spidev): from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() unicornhatmini.set_all(255, 255, 255) assert unicornhatmini.disp == [[255 >> 2, 255 >> 2, 255 >> 2]] * (17 * 7) def test_set_brightness(GPIO, spidev): from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() unicornhatmini.set_brightness(0.5) def test_show(GPIO, spidev): from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() unicornhatmini.show() def test_clear(GPIO, spidev): from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() unicornhatmini.clear() assert unicornhatmini.disp == [[0, 0, 0]] * (17 * 7) def test_set_rotation(GPIO, spidev): from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() shapes = { 0: (17, 7), 90: (7, 17), 180: (17, 7), 270: (7, 17) } for rotation in (0, 90, 180, 270): unicornhatmini.set_rotation(rotation) assert unicornhatmini.get_shape() == shapes[rotation] unicornhatmini.set_pixel(0, 0, 255, 255, 255) def test_set_rotation_invalid(GPIO, spidev): from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() with pytest.raises(ValueError): unicornhatmini.set_rotation(99) def test_set_image(GPIO, spidev): from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() image = mock.MagicMock() image.size = (17, 7) image.getpixel.return_value = (255, 255, 255) image.convert.return_value = image unicornhatmini.set_image(image, offset_x=0, offset_y=0) image.mode = "RGB" unicornhatmini.set_image(image, offset_x=0, offset_y=0) def test_set_image_wrap(GPIO, spidev): from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() image = mock.MagicMock() image.size = (3, 3) image.mode = "RGB" image.getpixel.return_value = (255, 255, 255) unicornhatmini.set_image(image, offset_x=0, offset_y=0, wrap=True) ./library/MANIFEST.in0000664000175000017500000000016314223641750015347 0ustar jawn-smithjawn-smithinclude CHANGELOG.txt include LICENSE.txt include README.md include setup.py recursive-include unicornhatmini *.py ./library/setup.cfg0000664000175000017500000000223314223641750015432 0ustar jawn-smithjawn-smith# -*- coding: utf-8 -*- [metadata] name = unicornhatmini version = 0.0.2 author = Philip Howard author_email = phil@pimoroni.com description = Python library for the unicornhatmini 17x7 RGB LED matrix long_description = file: README.md long_description_content_type = text/markdown keywords = Raspberry Pi url = https://www.pimoroni.com project_urls = GitHub=https://www.github.com/pimoroni/unicornhatmini-python license = MIT # This includes the license file(s) in the wheel. # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file license_files = LICENSE.txt classifiers = Development Status :: 4 - Beta Operating System :: POSIX :: Linux License :: OSI Approved :: MIT License Intended Audience :: Developers Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Topic :: Software Development Topic :: Software Development :: Libraries Topic :: System :: Hardware [options] python_requires = >= 2.7 packages = unicornhatmini install_requires = spidev [flake8] exclude = .tox, .eggs, .git, __pycache__, build, dist ignore = E501 [pimoroni] py2deps = py3deps = configtxt = commands = ./library/pyproject.toml0000664000175000017500000000014214223641750016522 0ustar jawn-smithjawn-smith[build-system] requires = ["setuptools>=40.8.0", "wheel"] build-backend = "setuptools.build_meta" ./library/.coveragerc0000664000175000017500000000006014223641750015726 0ustar jawn-smithjawn-smith[run] source = unicornhatmini omit = .tox/* ./library/CHANGELOG.txt0000664000175000017500000000015714223641750015644 0ustar jawn-smithjawn-smith0.0.2 ----- * Fix for Pi kernel 5.4.51, removed cshigh setting from SPI setup 0.0.1 ----- * Initial Release ./library/setup.py0000775000175000017500000000255114223641750015331 0ustar jawn-smithjawn-smith#!/usr/bin/env python # -*- coding: utf-8 -*- """ Copyright (c) 2016 Pimoroni Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ from setuptools import setup, __version__ from pkg_resources import parse_version minimum_version = parse_version('30.4.0') if parse_version(__version__) < minimum_version: raise RuntimeError("Package setuptools must be at least version {}".format(minimum_version)) setup() ./library/LICENSE.txt0000664000175000017500000000205614223641750015437 0ustar jawn-smithjawn-smithMIT License Copyright (c) 2018 Pimoroni Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ./library/tox.ini0000664000175000017500000000061514223641750015126 0ustar jawn-smithjawn-smith[tox] envlist = py{35,37,39},qa skip_missing_interpreters = True [testenv] commands = python setup.py install coverage run -m py.test -v -r wsx coverage report -m deps = mock pytest>=3.1 pytest-cov [testenv:qa] commands = check-manifest --ignore tox.ini,tests*,.coveragerc python setup.py sdist bdist_wheel twine check dist/* flake8 --ignore E501 deps = check-manifest flake8 twine ./library/unicornhatmini/0000775000175000017500000000000014223641750016640 5ustar jawn-smithjawn-smith./library/unicornhatmini/__init__.py0000664000175000017500000001607214223641750020757 0ustar jawn-smithjawn-smith#!/usr/bin/env python3 import time import atexit import spidev from colorsys import hsv_to_rgb import RPi.GPIO as GPIO __version__ = '0.0.2' # Holtek HT16D35 CMD_SOFT_RESET = 0xCC CMD_GLOBAL_BRIGHTNESS = 0x37 CMD_COM_PIN_CTRL = 0x41 CMD_ROW_PIN_CTRL = 0x42 CMD_WRITE_DISPLAY = 0x80 CMD_READ_DISPLAY = 0x81 CMD_SYSTEM_CTRL = 0x35 CMD_SCROLL_CTRL = 0x20 _COLS = 17 _ROWS = 7 BUTTON_A = 5 BUTTON_B = 6 BUTTON_X = 16 BUTTON_Y = 20 class UnicornHATMini(): lut = [[139, 138, 137], [223, 222, 221], [167, 166, 165], [195, 194, 193], [111, 110, 109], [55, 54, 53], [83, 82, 81], [136, 135, 134], [220, 219, 218], [164, 163, 162], [192, 191, 190], [108, 107, 106], [52, 51, 50], [80, 79, 78], [113, 115, 114], [197, 199, 198], [141, 143, 142], [169, 171, 170], [85, 87, 86], [29, 31, 30], [57, 59, 58], [116, 118, 117], [200, 202, 201], [144, 146, 145], [172, 174, 173], [88, 90, 89], [32, 34, 33], [60, 62, 61], [119, 121, 120], [203, 205, 204], [147, 149, 148], [175, 177, 176], [91, 93, 92], [35, 37, 36], [63, 65, 64], [122, 124, 123], [206, 208, 207], [150, 152, 151], [178, 180, 179], [94, 96, 95], [38, 40, 39], [66, 68, 67], [125, 127, 126], [209, 211, 210], [153, 155, 154], [181, 183, 182], [97, 99, 98], [41, 43, 42], [69, 71, 70], [128, 130, 129], [212, 214, 213], [156, 158, 157], [184, 186, 185], [100, 102, 101], [44, 46, 45], [72, 74, 73], [131, 133, 132], [215, 217, 216], [159, 161, 160], [187, 189, 188], [103, 105, 104], [47, 49, 48], [75, 77, 76], [363, 362, 361], [447, 446, 445], [391, 390, 389], [419, 418, 417], [335, 334, 333], [279, 278, 277], [307, 306, 305], [360, 359, 358], [444, 443, 442], [388, 387, 386], [416, 415, 414], [332, 331, 330], [276, 275, 274], [304, 303, 302], [337, 339, 338], [421, 423, 422], [365, 367, 366], [393, 395, 394], [309, 311, 310], [253, 255, 254], [281, 283, 282], [340, 342, 341], [424, 426, 425], [368, 370, 369], [396, 398, 397], [312, 314, 313], [256, 258, 257], [284, 286, 285], [343, 345, 344], [427, 429, 428], [371, 373, 372], [399, 401, 400], [315, 317, 316], [259, 261, 260], [287, 289, 288], [346, 348, 347], [430, 432, 431], [374, 376, 375], [402, 404, 403], [318, 320, 319], [262, 264, 263], [290, 292, 291], [349, 351, 350], [433, 435, 434], [377, 379, 378], [405, 407, 406], [321, 323, 322], [265, 267, 266], [293, 295, 294], [352, 354, 353], [436, 438, 437], [380, 382, 381], [408, 410, 409], [324, 326, 325], [268, 270, 269], [296, 298, 297]] def __init__(self, spi_max_speed_hz=600000): """Initialise unicornhatmini Tested to around 6MHz (500fps) on a Pi 4 and 6KHz (50fps) on an A+. :param spi_max_speed_hz: SPI speed in Hz """ self.disp = [[0, 0, 0] for _ in range(_COLS * _ROWS)] self.left_matrix = (spidev.SpiDev(0, 0), 8, 0) self.right_matrix = (spidev.SpiDev(0, 1), 7, 28 * 8) GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) self.buf = [0 for _ in range(28 * 8 * 2)] self._rotation = 0 for device, pin, offset in self.left_matrix, self.right_matrix: device.no_cs = True device.max_speed_hz = spi_max_speed_hz GPIO.setup(pin, GPIO.OUT, initial=GPIO.HIGH) self.xfer(device, pin, [CMD_SOFT_RESET]) self.xfer(device, pin, [CMD_GLOBAL_BRIGHTNESS, 0x01]) self.xfer(device, pin, [CMD_SCROLL_CTRL, 0x00]) self.xfer(device, pin, [CMD_SYSTEM_CTRL, 0x00]) self.xfer(device, pin, [CMD_WRITE_DISPLAY, 0x00] + self.buf[offset:offset + (28 * 8)]) self.xfer(device, pin, [CMD_COM_PIN_CTRL, 0xff]) self.xfer(device, pin, [CMD_ROW_PIN_CTRL, 0xff, 0xff, 0xff, 0xff]) self.xfer(device, pin, [CMD_SYSTEM_CTRL, 0x03]) atexit.register(self._exit) def _shutdown(self): for device, pin, _ in self.left_matrix, self.right_matrix: self.xfer(device, pin, [CMD_COM_PIN_CTRL, 0x00]) self.xfer(device, pin, [CMD_ROW_PIN_CTRL, 0x00, 0x00, 0x00, 0x00]) self.xfer(device, pin, [CMD_SYSTEM_CTRL, 0x00]) def _exit(self): self._shutdown() def xfer(self, device, pin, command): GPIO.output(pin, GPIO.LOW) device.xfer2(command) GPIO.output(pin, GPIO.HIGH) def set_pixel(self, x, y, r, g, b): """Set a single pixel.""" offset = (x * _ROWS) + y if self._rotation == 90: y = _COLS - 1 - y offset = (y * _ROWS) + x if self._rotation == 180: x = _COLS - 1 - x y = _ROWS - 1 - y offset = (x * _ROWS) + y if self._rotation == 270: x = _ROWS - 1 - x offset = (y * _ROWS) + x self.disp[offset] = [r >> 2, g >> 2, b >> 2] def set_all(self, r, g, b): """Set all pixels.""" r >>= 2 g >>= 2 b >>= 2 for i in range(_ROWS * _COLS): self.disp[i] = [r, g, b] def set_image(self, image, offset_x=0, offset_y=0, wrap=False, bg_color=(0, 0, 0)): """Set a PIL image to the display buffer.""" image_width, image_height = image.size if image.mode != "RGB": image = image.convert('RGB') display_width, display_height = self.get_shape() for y in range(display_height): for x in range(display_width): r, g, b = bg_color i_x = x + offset_x i_y = y + offset_y if wrap: while i_x >= image_width: i_x -= image_width while i_y >= image_height: i_y -= image_height if i_x < image_width and i_y < image_height: r, g, b = image.getpixel((i_x, i_y)) self.set_pixel(x, y, r, g, b) def clear(self): """Set all pixels to 0.""" self.set_all(0, 0, 0) def set_brightness(self, b=0.2): for device, pin, _ in self.left_matrix, self.right_matrix: self.xfer(device, pin, [CMD_GLOBAL_BRIGHTNESS, int(63 * b)]) def set_rotation(self, rotation=0): if rotation not in [0, 90, 180, 270]: raise ValueError("Rotation must be one of 0, 90, 180, 270") self._rotation = rotation def show(self): for i in range(_COLS * _ROWS): ir, ig, ib = self.lut[i] r, g, b = self.disp[i] self.buf[ir] = r self.buf[ig] = g self.buf[ib] = b for device, pin, offset in self.left_matrix, self.right_matrix: self.xfer(device, pin, [CMD_WRITE_DISPLAY, 0x00] + self.buf[offset:offset + (28 * 8)]) def get_shape(self): if self._rotation in [90, 270]: return _ROWS, _COLS else: return _COLS, _ROWS if __name__ == "__main__": unicornhatmini = UnicornHATMini() while True: for y in range(_ROWS): for x in range(_COLS): hue = (time.time() / 4.0) + (x / float(_COLS * 2)) + (y / float(_ROWS)) r, g, b = [int(c * 255) for c in hsv_to_rgb(hue, 1.0, 1.0)] unicornhatmini.set_pixel(x, y, r, g, b) unicornhatmini.show() time.sleep(1.0 / 60) ./library/README.md0000664000175000017500000000213314223641750015067 0ustar jawn-smithjawn-smith# Unicorn HAT Mini [![Build Status](https://travis-ci.com/pimoroni/unicornhatmini-python.svg?branch=master)](https://travis-ci.com/pimoroni/unicornhatmini-python) [![Coverage Status](https://coveralls.io/repos/github/pimoroni/unicornhatmini-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/unicornhatmini-python?branch=master) [![PyPi Package](https://img.shields.io/pypi/v/unicornhatmini.svg)](https://pypi.python.org/pypi/unicornhatmini) [![Python Versions](https://img.shields.io/pypi/pyversions/unicornhatmini.svg)](https://pypi.python.org/pypi/unicornhatmini) # Requirements You must enable SPI on your Raspberry Pi: * Run: `sudo raspi-config nonint do_spi 0` # Installing Stable library from PyPi: * Just run `sudo pip3 install unicornhatmini` Or for Python 2: * `sudo pip install unicornhatmini` Latest/development library from GitHub: * `git clone https://github.com/pimoroni/unicornhatmini-python` * `cd unicornhatmini-python` * `sudo ./install.sh` # Changelog 0.0.2 ----- * Fix for Pi kernel 5.4.51, removed cshigh setting from SPI setup 0.0.1 ----- * Initial Release ./README.md0000664000175000017500000000173714223641750013434 0ustar jawn-smithjawn-smith# Unicorn HAT Mini [![Build Status](https://travis-ci.com/pimoroni/unicornhatmini-python.svg?branch=master)](https://travis-ci.com/pimoroni/unicornhatmini-python) [![Coverage Status](https://coveralls.io/repos/github/pimoroni/unicornhatmini-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/unicornhatmini-python?branch=master) [![PyPi Package](https://img.shields.io/pypi/v/unicornhatmini.svg)](https://pypi.python.org/pypi/unicornhatmini) [![Python Versions](https://img.shields.io/pypi/pyversions/unicornhatmini.svg)](https://pypi.python.org/pypi/unicornhatmini) # Requirements You must enable SPI on your Raspberry Pi: * Run: `sudo raspi-config nonint do_spi 0` # Installing Stable library from PyPi: * Just run `sudo pip3 install unicornhatmini` Or for Python 2: * `sudo pip install unicornhatmini` Latest/development library from GitHub: * `git clone https://github.com/pimoroni/unicornhatmini-python` * `cd unicornhatmini-python` * `sudo ./install.sh` ./install.sh0000775000175000017500000001265214223641750014160 0ustar jawn-smithjawn-smith#!/bin/bash CONFIG=/boot/config.txt DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` CONFIG_BACKUP=false APT_HAS_UPDATED=false USER_HOME=/home/$SUDO_USER RESOURCES_TOP_DIR=$USER_HOME/Pimoroni WD=`pwd` USAGE="sudo ./install.sh (--unstable)" POSITIONAL_ARGS=() UNSTABLE=false CODENAME=`lsb_release -sc` if [[ $CODENAME == "bullseye" ]]; then bash ./install-bullseye.sh exit $? fi user_check() { if [ $(id -u) -ne 0 ]; then printf "Script must be run as root. Try 'sudo ./install.sh'\n" exit 1 fi } confirm() { if [ "$FORCE" == '-y' ]; then true else read -r -p "$1 [y/N] " response < /dev/tty if [[ $response =~ ^(yes|y|Y)$ ]]; then true else false fi fi } prompt() { read -r -p "$1 [y/N] " response < /dev/tty if [[ $response =~ ^(yes|y|Y)$ ]]; then true else false fi } success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" } inform() { echo -e "$(tput setaf 6)$1$(tput sgr0)" } warning() { echo -e "$(tput setaf 1)$1$(tput sgr0)" } function do_config_backup { if [ ! $CONFIG_BACKUP == true ]; then CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" inform "Backing up $CONFIG to /boot/$FILENAME\n" cp $CONFIG /boot/$FILENAME mkdir -p $RESOURCES_TOP_DIR/config-backups/ cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME if [ -f "$UNINSTALLER" ]; then echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER fi fi } function apt_pkg_install { PACKAGES=() PACKAGES_IN=("$@") for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do PACKAGE="${PACKAGES_IN[$i]}" if [ "$PACKAGE" == "" ]; then continue; fi printf "Checking for $PACKAGE\n" dpkg -L $PACKAGE > /dev/null 2>&1 if [ "$?" == "1" ]; then PACKAGES+=("$PACKAGE") fi done PACKAGES="${PACKAGES[@]}" if ! [ "$PACKAGES" == "" ]; then echo "Installing missing packages: $PACKAGES" if [ ! $APT_HAS_UPDATED ]; then apt update APT_HAS_UPDATED=true fi apt install -y $PACKAGES if [ -f "$UNINSTALLER" ]; then echo "apt uninstall -y $PACKAGES" fi fi } while [[ $# -gt 0 ]]; do K="$1" case $K in -u|--unstable) UNSTABLE=true shift ;; *) if [[ $1 == -* ]]; then printf "Unrecognised option: $1\n"; printf "Usage: $USAGE\n"; exit 1 fi POSITIONAL_ARGS+=("$1") shift esac done user_check apt_pkg_install python-configparser CONFIG_VARS=`python - < $UNINSTALLER printf "It's recommended you run these steps manually.\n" printf "If you want to run the full script, open it in\n" printf "an editor and remove 'exit 1' from below.\n" exit 1 EOF printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" if $UNSTABLE; then warning "Installing unstable library from source.\n\n" else printf "Installing stable library from pypi.\n\n" fi cd library printf "Installing for Python 2..\n" apt_pkg_install "${PY2_DEPS[@]}" if $UNSTABLE; then python setup.py install > /dev/null else pip install --upgrade $LIBRARY_NAME fi if [ $? -eq 0 ]; then success "Done!\n" echo "pip uninstall $LIBRARY_NAME" >> $UNINSTALLER fi if [ -f "/usr/bin/python3" ]; then printf "Installing for Python 3..\n" apt_pkg_install "${PY3_DEPS[@]}" if $UNSTABLE; then python3 setup.py install > /dev/null else pip3 install --upgrade $LIBRARY_NAME fi if [ $? -eq 0 ]; then success "Done!\n" echo "pip3 uninstall $LIBRARY_NAME" >> $UNINSTALLER fi fi cd $WD for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do CMD="${SETUP_CMDS[$i]}" # Attempt to catch anything that touches /boot/config.txt and trigger a backup if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then do_config_backup fi eval $CMD done for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do CONFIG_LINE="${CONFIG_TXT[$i]}" if ! [ "$CONFIG_LINE" == "" ]; then do_config_backup inform "Adding $CONFIG_LINE to $CONFIG\n" sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG if ! grep -q "^$CONFIG_LINE" $CONFIG; then printf "$CONFIG_LINE\n" >> $CONFIG fi fi done if [ -d "examples" ]; then if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then inform "Copying examples to $RESOURCES_DIR" cp -r examples/ $RESOURCES_DIR echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER success "Done!" fi fi printf "\n" if [ -f "/usr/bin/pydoc" ]; then printf "Generating documentation.\n" pydoc -w $LIBRARY_NAME > /dev/null if [ -f "$LIBRARY_NAME.html" ]; then cp $LIBRARY_NAME.html $RESOURCES_DIR/docs.html rm -f $LIBRARY_NAME.html inform "Documentation saved to $RESOURCES_DIR/docs.html" success "Done!" else warning "Error: Failed to generate documentation." fi fi success "\nAll done!" inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" inform "Find uninstall steps in $UNINSTALLER\n" ./Makefile0000664000175000017500000000470514223641750013613 0ustar jawn-smithjawn-smithLIBRARY_VERSION=$(shell grep version library/setup.cfg | awk -F" = " '{print $$2}') LIBRARY_NAME=$(shell grep name library/setup.cfg | awk -F" = " '{print $$2}') .PHONY: usage install uninstall usage: @echo "Library: ${LIBRARY_NAME}" @echo "Version: ${LIBRARY_VERSION}\n" @echo "Usage: make , where target is one of:\n" @echo "install: install the library locally from source" @echo "uninstall: uninstall the local library" @echo "check: peform basic integrity checks on the codebase" @echo "python-readme: generate library/README.md from README.md + library/CHANGELOG.txt" @echo "python-wheels: build python .whl files for distribution" @echo "python-sdist: build python source distribution" @echo "python-clean: clean python build and dist directories" @echo "python-dist: build all python distribution files" @echo "python-testdeploy: build all and deploy to test PyPi" @echo "tag: tag the repository with the current version" install: ./install.sh uninstall: ./uninstall.sh check: @echo "Checking for trailing whitespace" @! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO @echo "Checking for DOS line-endings" @! grep -IUrn --color " " --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile @echo "Checking library/CHANGELOG.txt" @cat library/CHANGELOG.txt | grep ^${LIBRARY_VERSION} @echo "Checking library/${LIBRARY_NAME}/__init__.py" @cat library/${LIBRARY_NAME}/__init__.py | grep "^__version__ = '${LIBRARY_VERSION}'" tag: git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" python-readme: library/README.md python-license: library/LICENSE.txt library/README.md: README.md library/CHANGELOG.txt cp README.md library/README.md printf "\n# Changelog\n" >> library/README.md cat library/CHANGELOG.txt >> library/README.md library/LICENSE.txt: LICENSE cp LICENSE library/LICENSE.txt python-wheels: python-readme python-license cd library; python3 setup.py bdist_wheel cd library; python setup.py bdist_wheel python-sdist: python-readme python-license cd library; python setup.py sdist python-clean: -rm -r library/dist -rm -r library/build -rm -r library/*.egg-info python-dist: python-clean python-wheels python-sdist ls library/dist python-testdeploy: python-dist twine upload --repository-url https://test.pypi.org/legacy/ library/dist/* python-deploy: check python-dist twine upload library/dist/* ./install-bullseye.sh0000775000175000017500000001272314223641750016001 0ustar jawn-smithjawn-smith#!/bin/bash CONFIG=/boot/config.txt DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` CONFIG_BACKUP=false APT_HAS_UPDATED=false USER_HOME=/home/$SUDO_USER RESOURCES_TOP_DIR=$USER_HOME/Pimoroni WD=`pwd` USAGE="sudo ./install.sh (--unstable)" POSITIONAL_ARGS=() UNSTABLE=false PYTHON="/usr/bin/python3" CODENAME=`lsb_release -sc` distro_check() { if [[ $CODENAME != "bullseye" ]]; then printf "This installer is for Raspberry Pi OS: Bullseye only, current distro: $CODENAME\n" exit 1 fi } user_check() { if [ $(id -u) -ne 0 ]; then printf "Script must be run as root. Try 'sudo ./install.sh'\n" exit 1 fi } confirm() { if [ "$FORCE" == '-y' ]; then true else read -r -p "$1 [y/N] " response < /dev/tty if [[ $response =~ ^(yes|y|Y)$ ]]; then true else false fi fi } prompt() { read -r -p "$1 [y/N] " response < /dev/tty if [[ $response =~ ^(yes|y|Y)$ ]]; then true else false fi } success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" } inform() { echo -e "$(tput setaf 6)$1$(tput sgr0)" } warning() { echo -e "$(tput setaf 1)$1$(tput sgr0)" } function do_config_backup { if [ ! $CONFIG_BACKUP == true ]; then CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" inform "Backing up $CONFIG to /boot/$FILENAME\n" cp $CONFIG /boot/$FILENAME mkdir -p $RESOURCES_TOP_DIR/config-backups/ cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME if [ -f "$UNINSTALLER" ]; then echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER fi fi } function apt_pkg_install { PACKAGES=() PACKAGES_IN=("$@") for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do PACKAGE="${PACKAGES_IN[$i]}" if [ "$PACKAGE" == "" ]; then continue; fi printf "Checking for $PACKAGE\n" dpkg -L $PACKAGE > /dev/null 2>&1 if [ "$?" == "1" ]; then PACKAGES+=("$PACKAGE") fi done PACKAGES="${PACKAGES[@]}" if ! [ "$PACKAGES" == "" ]; then echo "Installing missing packages: $PACKAGES" if [ ! $APT_HAS_UPDATED ]; then apt update APT_HAS_UPDATED=true fi apt install -y $PACKAGES if [ -f "$UNINSTALLER" ]; then echo "apt uninstall -y $PACKAGES" fi fi } while [[ $# -gt 0 ]]; do K="$1" case $K in -u|--unstable) UNSTABLE=true shift ;; -p|--python) PYTHON=$2 shift shift ;; *) if [[ $1 == -* ]]; then printf "Unrecognised option: $1\n"; printf "Usage: $USAGE\n"; exit 1 fi POSITIONAL_ARGS+=("$1") shift esac done distro_check user_check if [ ! -f "$PYTHON" ]; then printf "Python path $PYTHON not found!\n" exit 1 fi PYTHON_VER=`$PYTHON --version` inform "Installing. Please wait..." $PYTHON -m pip install --upgrade configparser CONFIG_VARS=`$PYTHON - < $UNINSTALLER printf "It's recommended you run these steps manually.\n" printf "If you want to run the full script, open it in\n" printf "an editor and remove 'exit 1' from below.\n" exit 1 EOF printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" if $UNSTABLE; then warning "Installing unstable library from source.\n\n" else printf "Installing stable library from pypi.\n\n" fi cd library printf "Installing for $PYTHON_VER...\n" apt_pkg_install "${PY3_DEPS[@]}" if $UNSTABLE; then $PYTHON setup.py install > /dev/null else $PYTHON -m pip install --upgrade $LIBRARY_NAME fi if [ $? -eq 0 ]; then success "Done!\n" echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER fi cd $WD for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do CMD="${SETUP_CMDS[$i]}" # Attempt to catch anything that touches /boot/config.txt and trigger a backup if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then do_config_backup fi eval $CMD done for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do CONFIG_LINE="${CONFIG_TXT[$i]}" if ! [ "$CONFIG_LINE" == "" ]; then do_config_backup inform "Adding $CONFIG_LINE to $CONFIG\n" sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG if ! grep -q "^$CONFIG_LINE" $CONFIG; then printf "$CONFIG_LINE\n" >> $CONFIG fi fi done if [ -d "examples" ]; then if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then inform "Copying examples to $RESOURCES_DIR" cp -r examples/ $RESOURCES_DIR echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER success "Done!" fi fi printf "\n" if [ -f "/usr/bin/pydoc" ]; then printf "Generating documentation.\n" pydoc -w $LIBRARY_NAME > /dev/null if [ -f "$LIBRARY_NAME.html" ]; then cp $LIBRARY_NAME.html $RESOURCES_DIR/docs.html rm -f $LIBRARY_NAME.html inform "Documentation saved to $RESOURCES_DIR/docs.html" success "Done!" else warning "Error: Failed to generate documentation." fi fi success "\nAll done!" inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" inform "Find uninstall steps in $UNINSTALLER\n" ./uninstall.sh0000775000175000017500000000106114223641750014513 0ustar jawn-smithjawn-smith#!/bin/bash LIBRARY_VERSION=`cat library/setup.cfg | grep version | awk -F" = " '{print $2}'` LIBRARY_NAME=`cat library/setup.cfg | grep name | awk -F" = " '{print $2}'` printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Uninstaller\n\n" if [ $(id -u) -ne 0 ]; then printf "Script must be run as root. Try 'sudo ./uninstall.sh'\n" exit 1 fi cd library printf "Uninstalling for Python 2..\n" pip uninstall $LIBRARY_NAME if [ -f "/usr/bin/pip3" ]; then printf "Uninstalling for Python 3..\n" pip3 uninstall $LIBRARY_NAME fi cd .. printf "Done!\n" ./LICENSE0000664000175000017500000000205614223641750013155 0ustar jawn-smithjawn-smithMIT License Copyright (c) 2018 Pimoroni Ltd. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ./REFERENCE.md0000664000175000017500000001537314223641750013776 0ustar jawn-smithjawn-smith# Reference - [Getting Started](#getting-started) - [Installing](#installing) - [Examples](#examples) - [Buttons](#buttons) - [Colour Cycle](#colour-cycle) - [Columns](#columns) - [Demo](#demo) - [Forest Fire](#forest-fire) - [FPS](#fps) - [Image](#image) - [Rainbow](#rainbow) - [Simon](#simon) - [Function Reference](#function-reference) - [Setting Pixels](#setting-pixels) - [Setting An Image](#setting-an-image) - [Changing Global Brightness](#changing-global-brightness) - [Clearing The Display](#clearing-the-display) - [Displaying The Result](#displaying-the-result) - [Using The Buttons](#using-the-buttons) ## Getting Started Unicorn HAT Mini includes an array of 17x7 LEDs you can control by setting individual pixels, or whole pre-prepared images. It also includes four buttons on BCM pins 5, 6, 16 and 24. We demonstrate using the GPIO Zero library with these. ### Installing Most people should grab the library from GitHub and run our simple installer: ``` git clone https://github.com/pimoroni/unicornhatmini-python cd unicornhatmini-python sudo ./install.sh ``` This will ensure any dependencies are installed and copy examples into `~/Pimoroni/unicornhatmini/` You can install just the Unicorn HAT Mini library by running: ``` sudo pip3 install unicornhatmini ``` ## Examples ### Buttons [buttons.py](examples/buttons.py) Demonstrates the use of Unicorn HAT Mini's buttons with the GPIO Zero library. ### Colour Cycle [colour-cycle.py](examples/colour-cycle.py) Cycles through colour hues across all of Unicorn HAT Mini's pixels. ### Columns [columns.py](examples/columns.py) Stack columns of coloured lights in this simple game. Button Y will rotate the order of the coloured lights in a column. Button X will move the column to the right, wrapping around to the left of the screen if it crosses the right edge. ### Demo [demo.py](examples/demo.py) Displays a sequence of 4 demoscene style effects across Unicorn HAT mini. 1. Tunnel effect 2. Rainbow Searchlights effect 3. Checkerboard effect 4. Swirl effect ### Forest Fire [forest-fire.py](examples/forest-fire.py) A classic cellular automata simulating the growth of forest (green lights) and outbreak of fires (red lights). ### FPS [fps.py](examples/fps.py) Will try to refresh the Unicorn HAT Mini display as fast as possible and display the framerate in the terminal. You can change the update rate, and consequently the framerate, by changing the `spi_max_speed_hz` parameter when constructing a new `UnicornHATMini()` instance. ### Image [image.py](examples/image.py) Demonstrates the use of PIL (Python Image Library) to create an image and display it upon Unicorn HAT Mini. In this case it's scrolling an image longer than the display to produce a wavy line pattern. Whether you're scrolling text or displaying a UI for your project, you'll probably use PIL. ### Rainbow [rainbow.py](examples/rainbow.py) Displays a concentric rainbow that moves around the Unicorn HAT Mini display. ### Simon [simon.py](examples/simon.py) A simple game. Repeat the colour sequence at each step to proceed and see how long a sequence you can memorise. Colours will flash up adjacent to their corresponding buttons. ## Function Reference In all cases you'll need to first initialise the Unicorn HAT Mini library like so: ```python from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() ``` `UnicornHATMini` takes one argument, `spi_max_speed_hz` which you can use to allow faster SPI speeds and achieve better framerates. In most cases, however, how much you try to draw will have a bigger impact. ### Setting Pixels You can set either one or all pixels with `set_pixel` or `set_all` respectively. #### set_pixel `set_pixel` requires an `x` and `y` value starting from `0,0` and ending at `16,6` (zero indexed, 17 by 7 pixel display). For example at normal rotation the top left corner would be `0,0` and the bottom right `16,6`. A handy mnemonic for remembering which is left/right and which is up/down is "Along the corridor and down the stairs." If you rotate Unicorn HAT Mini by `90` or `270` degrees then it becomes 7 by 17 pixels in size, so your coordinates must change accordingly. `set_pixel` also requires three colour values, one for red, one for green and one for blue. For example `unicornhatmini.set_pixel(0, 0, 255, 0, 0)` would set the top left pixel to red. #### set_all `set_all` is very similar to `set_pixel` except it does not require an `x` and `y` coordinate, since the colour you're setting applies to all pixels. For example `unicornhatmini.set_all(0, 255, 0)` would set all pixels to green. ### Setting An Image For any complex drawing - such as text, images and icons - you will want to use the Python Image Library to create an image, and `set_image` to copy it to Unicorn HAT Mini. `set_image` accepts a PIL image of any size, so you can display part of a larger image to achieve scrolling affects. To set a blank red PIL image to Unicorn HAT Mini you might: ```python from PIL import Image from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() image = Image.new("RGB", (17, 7), (255, 0, 0)) unicornhatmini.set_image(image) unicornhatmini.show() ``` If you've drawn an image you want to display and saved it to a file, you could do something like: ```python from PIL import Image from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() image = Image.open("my-image.png") unicornhatmini.set_image(image) unicornhatmini.show() ``` ### Changing Global Brightness Unicorn HAT Mini has a global brightness setting which you can change with `unicornhatmini.set_brightness`. It accepts a value from `0.0` to `1.0` so for half brightness you would use: ```python unicornhatmini.set_brightness(0.5) ``` ### Clearing The Display You can clear Unicorn HAT Mini to black using `unicornhatmini.clear()`. This is just an alias for `unicornhatmini.set_all(0, 0, 0)` which does the same thing. ### Displaying The Result To send your image or carefully curated pixels to Unicorn HAT Mini you should call `unicornhatmini.show()`. This transfers the local buffer over to the display, and you'll usually want to call this once per frame after all of your drawing operations. ### Using The Buttons To use the buttons on your Unicorn HAT Mini you should use the GPIO Zero library. Unicorn HAT Mini has four buttons on pins BCM 5, 6, 16 and 24. The example below shows how you might bind them to a function: ```python from gpiozero import Button def pressed(pin): print(f"Button on pin {pin} pressed!") button_a = Button(5) button_b = Button(6) button_x = Button(16) button_y = Button(24) button_a.when_pressed = pressed button_b.when_pressed = pressed button_x.when_pressed = pressed button_y.when_pressed = pressed ``` ./examples/0000775000175000017500000000000014223641750013763 5ustar jawn-smithjawn-smith./examples/button-splash.py0000775000175000017500000000411014223641750017137 0ustar jawn-smithjawn-smith#!/usr/bin/env python3 import time import math from gpiozero import Button from unicornhatmini import UnicornHATMini from colorsys import hsv_to_rgb print("""Unicorn HAT Mini: buttons.py Demonstrates the use of Unicorn HAT Mini's buttons with gpiozero. Splashes a rainbow across the screen, originating at the pressed button. Press Ctrl+C to exit! """) unicornhatmini = UnicornHATMini() unicornhatmini.set_brightness(0.5) unicornhatmini.set_rotation(0) width, height = unicornhatmini.get_shape() splash_origin = (0, 0) splash_time = 0 def pressed(button): global splash_origin, splash_time button_name, x, y = button_map[button.pin.number] splash_origin = (x, y) splash_time = time.time() print(f"Button {button_name} pressed!") button_map = {5: ("A", 0, 0), # Top Left 6: ("B", 0, 6), # Bottom Left 16: ("X", 16, 0), # Top Right 24: ("Y", 16, 7)} # Bottom Right button_a = Button(5) button_b = Button(6) button_x = Button(16) button_y = Button(24) def distance(x1, y1, x2, y2): return math.sqrt(((x2 - x1) ** 2) + ((y2 - y1) ** 2)) try: button_a.when_pressed = pressed button_b.when_pressed = pressed button_x.when_pressed = pressed button_y.when_pressed = pressed while True: if splash_time > 0: splash_x, splash_y = splash_origin splash_progress = time.time() - splash_time for x in range(width): for y in range(height): d = distance(x, y, splash_x, splash_y) if (d / 30.0) < splash_progress and splash_progress < 0.6: h = d / 17.0 r, g, b = [int(c * 255) for c in hsv_to_rgb(h, 1.0, 1.0)] unicornhatmini.set_pixel(x, y, r, g, b) elif (d / 30.0) < splash_progress - 0.6: unicornhatmini.set_pixel(x, y, 0, 0, 0) unicornhatmini.show() time.sleep(1.0 / 60.0) except KeyboardInterrupt: button_a.close() button_b.close() button_x.close() button_y.close() ./examples/buttons.py0000775000175000017500000000141414223641750016036 0ustar jawn-smithjawn-smith#!/usr/bin/env python3 from gpiozero import Button from signal import pause print("""Unicorn HAT Mini: buttons.py Demonstrates the use of Unicorn HAT Mini's buttons with gpiozero. Press Ctrl+C to exit! """) def pressed(button): button_name = button_map[button.pin.number] print(f"Button {button_name} pressed!") button_map = {5: "A", 6: "B", 16: "X", 24: "Y"} button_a = Button(5) button_b = Button(6) button_x = Button(16) button_y = Button(24) try: button_a.when_pressed = pressed button_b.when_pressed = pressed button_x.when_pressed = pressed button_y.when_pressed = pressed pause() except KeyboardInterrupt: button_a.close() button_b.close() button_x.close() button_y.close() ./examples/forest-fire.py0000775000175000017500000000501514223641750016566 0ustar jawn-smithjawn-smith#!/usr/bin/env python3 # Forest fire model cellular automaton. Simulates the growth # of trees in a forest, with sporadic outbreaks of forest fires. # https://en.wikipedia.org/wiki/Forest-fire_model # Based on Rosetta Code Python Forest Fire example. # https://rosettacode.org/wiki/Forest_fire#Python import random import time from unicornhatmini import UnicornHATMini print(""" Unicorn HAT Mini: forest-fire.py Forest fire model cellular automaton. Simulates the growth of trees in a forest, with sporadic outbreaks of forest fires. Press Ctrl+C to exit! """) # Avoid retina-searage! unicornhatmini = UnicornHATMini() unicornhatmini.set_brightness(0.1) unicornhatmini.set_rotation(0) # The height and width of the forest. width, height = unicornhatmini.get_shape() # Initial probability of a grid square having a tree initial_trees = 0.55 # p = probability of tree growing, f = probability of fire p = 0.01 f = 0.0005 # Brightness values for a tree, fire, and blank space tree = [0, 255, 0] burning = [255, 0, 0] space = [0, 0, 0] # Each square's neighbour coordinates hood = ((-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)) # Function to populate the initial forest def initialise(): grid = {(x, y): (tree if random.random() <= initial_trees else space) for x in range(width) for y in range(height)} return grid # Display the forest, in its current state, on UnicornHATMini def show_grid(grid): unicornhatmini.clear() for x in range(width): for y in range(height): unicornhatmini.set_pixel(x, y, *grid[(x, y)]) unicornhatmini.show() # Go through grid, update grid squares based on state of # square and neighbouring squares def update_grid(grid): new_grid = {} for x in range(width): for y in range(height): if grid[(x, y)] == burning: new_grid[(x, y)] = space elif grid[(x, y)] == space: new_grid[(x, y)] = tree if random.random() <= p else space elif grid[(x, y)] == tree: new_grid[(x, y)] = (burning if any(grid.get((x + dx, y + dy), space) == burning for dx, dy in hood) or random.random() <= f else tree) return new_grid # Main function. Initialises grid, then shows, updates, and # waits for 1/60 of a second. def main(): grid = initialise() while True: show_grid(grid) grid = update_grid(grid) time.sleep(1 / 60.0) # Catches control-c and exits cleanly try: main() except KeyboardInterrupt: print("Exiting") # FIN! ./examples/text.py0000775000175000017500000000420314223641750015323 0ustar jawn-smithjawn-smith#!/usr/bin/env python3 import time import sys from colorsys import hsv_to_rgb from PIL import Image, ImageDraw, ImageFont from unicornhatmini import UnicornHATMini # The text we want to display. You should probably keep this line and replace it below # That way you'll have a guide as to what characters are supported! text = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 #@&!?{}<>[]();:.,'%*=+-=$_\\/ :-)" text = "Where did it all go wrong? :|" text = "Hello Hodgeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeey!!!" text = "I am the great and mighty Oz!" unicornhatmini = UnicornHATMini() rotation = 0 if len(sys.argv) > 1: try: rotation = int(sys.argv[1]) except ValueError: print("Usage: {} ".format(sys.argv[0])) sys.exit(1) unicornhatmini.set_rotation(rotation) display_width, display_height = unicornhatmini.get_shape() print("{}x{}".format(display_width, display_height)) # Do not look at unicornhatmini with remaining eye unicornhatmini.set_brightness(0.1) # Load a nice 5x7 pixel font # Granted it's actually 5x8 for some reason :| but that doesn't matter font = ImageFont.truetype("5x7.ttf", 8) # Measure the size of our text, we only really care about the width for the moment # but we could do line-by-line scroll if we used the height text_width, text_height = font.getsize(text) # Create a new PIL image big enough to fit the text image = Image.new('P', (text_width + display_width + display_width, display_height), 0) draw = ImageDraw.Draw(image) # Draw the text into the image draw.text((display_width, -1), text, font=font, fill=255) offset_x = 0 while True: for y in range(display_height): for x in range(display_width): hue = (time.time() / 10.0) + (x / float(display_width * 2)) r, g, b = [int(c * 255) for c in hsv_to_rgb(hue, 1.0, 1.0)] if image.getpixel((x + offset_x, y)) == 255: unicornhatmini.set_pixel(x, y, r, g, b) else: unicornhatmini.set_pixel(x, y, 0, 0, 0) offset_x += 1 if offset_x + display_width > image.size[0]: offset_x = 0 unicornhatmini.show() time.sleep(0.05) ./examples/demo.py0000775000175000017500000001060414223641750015265 0ustar jawn-smithjawn-smith#!/usr/bin/env python3 import colorsys import math import time from unicornhatmini import UnicornHATMini print("""Unicorn HAT Mini: demo.py This pixel shading demo transitions between 4 classic graphics demo effects. Press Ctrl+C to exit! """) unicornhatmini = UnicornHATMini() unicornhatmini.set_brightness(0.1) unicornhatmini.set_rotation(0) u_width, u_height = unicornhatmini.get_shape() # Generate a lookup table for 8-bit hue to RGB conversion hue_to_rgb = [] for i in range(0, 360): hue_to_rgb.append(colorsys.hsv_to_rgb(i / 359.0, 1, 1)) # Twisty swirly goodness def swirl(x, y, step): x -= (u_width / 2) y -= (u_height / 2) dist = math.sqrt(pow(x, 2) + pow(y, 2)) / 2.0 angle = (step / 10.0) + (dist * 1.5) s = math.sin(angle) c = math.cos(angle) xs = x * c - y * s ys = x * s + y * c r = abs(xs + ys) r = r * 12.0 r -= 20 return (r, r + (s * 130), r + (c * 130)) # Roto-zooming checker board def checker(x, y, step): x -= (u_width / 2) y -= (u_height / 2) angle = (step / 10.0) s = math.sin(angle) c = math.cos(angle) xs = x * c - y * s ys = x * s + y * c xs -= math.sin(step / 200.0) * 40.0 ys -= math.cos(step / 200.0) * 40.0 scale = step % 20 scale /= 20 scale = (math.sin(step / 50.0) / 8.0) + 0.25 xs *= scale ys *= scale xo = abs(xs) - int(abs(xs)) yo = abs(ys) - int(abs(ys)) v = 0 if (math.floor(xs) + math.floor(ys)) % 2 else 1 if xo > .1 and yo > .1 else .5 r, g, b = hue_to_rgb[int(step) % 255] return (r * (v * 255), g * (v * 255), b * (v * 255)) # Weeee waaaah def blues_and_twos(x, y, step): x -= (u_width / 2) y -= (u_height / 2) scale = math.sin(step / 6.0) / 1.5 r = math.sin((x * scale) / 1.0) + math.cos((y * scale) / 1.0) b = math.sin(x * scale / 2.0) + math.cos(y * scale / 2.0) g = r - .8 g = 0 if g < 0 else g b -= r b /= 1.4 return (r * 255, (b + g) * 255, g * 255) # Rainbow search spotlights def rainbow_search(x, y, step): xs = math.sin((step) / 100.0) * 20.0 ys = math.cos((step) / 100.0) * 20.0 scale = ((math.sin(step / 60.0) + 1.0) / 5.0) + 0.2 r = math.sin((x + xs) * scale) + math.cos((y + xs) * scale) g = math.sin((x + xs) * scale) + math.cos((y + ys) * scale) b = math.sin((x + ys) * scale) + math.cos((y + ys) * scale) return (r * 255, g * 255, b * 255) # Zoom tunnel def tunnel(x, y, step): speed = step / 100.0 x -= (u_width / 2) y -= (u_height / 2) xo = math.sin(step / 27.0) * 2 yo = math.cos(step / 18.0) * 2 x += xo y += yo if y == 0: if x < 0: angle = -(math.pi / 2) else: angle = (math.pi / 2) else: angle = math.atan(x / y) if y > 0: angle += math.pi angle /= 2 * math.pi # convert angle to 0...1 range hyp = math.sqrt(math.pow(x, 2) + math.pow(y, 2)) shade = hyp / 2.1 shade = 1 if shade > 1 else shade angle += speed depth = speed + (hyp / 10) col1 = hue_to_rgb[int(step) % 359] col1 = (col1[0] * 0.8, col1[1] * 0.8, col1[2] * 0.8) col2 = hue_to_rgb[int(step) % 359] col2 = (col2[0] * 0.3, col2[1] * 0.3, col2[2] * 0.3) col = col1 if int(abs(angle * 6.0)) % 2 == 0 else col2 td = .3 if int(abs(depth * 3.0)) % 2 == 0 else 0 col = (col[0] + td, col[1] + td, col[2] + td) col = (col[0] * shade, col[1] * shade, col[2] * shade) return (col[0] * 255, col[1] * 255, col[2] * 255) effects = [tunnel, rainbow_search, checker, swirl] t_start = time.time() try: while True: t = time.time() - t_start step = (t * 50) f = t / 10.0 fx = int(f) % len(effects) next_fx = (fx + 1) % len(effects) for y in range(u_height): for x in range(u_width): r, g, b = effects[fx](x, y, step) if f % 1.0 > 0.75: r2, g2, b2 = effects[next_fx](x, y, step) ratio = (1.0 - (f % 1.0)) / 0.25 r = r * ratio + r2 * (1.0 - ratio) g = g * ratio + g2 * (1.0 - ratio) b = b * ratio + b2 * (1.0 - ratio) r = int(max(0, min(255, r))) g = int(max(0, min(255, g))) b = int(max(0, min(255, b))) unicornhatmini.set_pixel(x, y, r, g, b) unicornhatmini.show() time.sleep(1.0 / 60.0) except KeyboardInterrupt: pass ./examples/twister.png0000664000175000017500000001044414223641750016175 0ustar jawn-smithjawn-smithPNG  IHDR0tEXtSoftwareAdobe ImageReadyqe<IDATxlYI$IV6f>GdTWUs^pvdɂ%#fþAΌggU{dxeKHOYg_~Oq'I~۟V6Gw/Ri4xM:M$m[ⴧ%鬉ᭉj#i;(LF=WWo""hK3k Y˝4NOS鏴HSvО9޼֓#) ƥ@H>8ZkM΂'Z#>sYOR+e%-3S܋1L>(~ e+n*`&pI0[ŎSRLJb"M <u&iy_yf7a:C}F#m:\j!ղ-EeZMD_0Ai"ǣ0>V YJ:\0DMFe9_z@%zmb^;"Pe h(58hJԆfc-nNZZ9> ).p3`ORIWvgD2TV&.s40FӮ}bW8XJ>D>y+x۹am(./v7UKOdkƞr9?zo&Md.٪4Aaji Hj:51JoU6Ym;3-xe1 oSqg*L^i 2:oB`rw]kT0%u N)xKgE eΝ*1/tד#]u煵h\^\2`/0>D)dw=+\a5OMo h_"EEU[8rD)-/+`#:ǝc YGwK-a 2]T%9}6 d&Y\W\>K2xc;ۺ$+9WVh?7~zO8SFjLL:Uruo]0bC{j70 0:7+5O_VMTo`rm&A G}48;d*9K%0 ~so1o.HNxq|'6 [=fچ<Os{L |1ə&tY`Nڛ oŒww)gK-FOGyYW N.b4DաT4]jGu{>7Gֺ|vv0$Q :s"14C!3s{18-YXK:s:ЅW BCMTD,)kFk]2/q5,ـh`P8XuoH~=qy?6[ɧ춳mPjq ADuk|q;U73S0q45l ƛ&)ɔo~繅n`&V*bWC(Op9ђG7,D+>7F!~s aXBiJq\hN/!H@y>XOnºF0ݻVi@BhrtcVyUiA@nʃ-jm`Z VH,,]S첕ݘIL4Q$'#~lWMA@j3 \9g]# 4]eOC$*<: 6jcIe H۠m< sH(Q5gK! #_`ֵ]|hݽ!PFO/:wYHEgʯ?E/+95wTc 9|J9Z"L6`lB(] vs=Rk4^|+l=' 3m&80R/$ZG;6ӮƮB+VhE1o@XZmз8[5)I4~Z:MAj?[φ\ݼТU{GYѲPgVO(1M!V#'̘N =HDakܐrP.[) Sir_ipv U͝<?5N(CUB̨v<004m>fL+T4岆rM._`肙ate$]jQ|LaaǎR\N3aa e,N]$bjß|Y``w* ݻ2>Qd&ݨ5WюY#fEٜf+'KqF1)0/t!Ȯ \%g4d3Zb||g .+3M+Ysj .]KO?p--^g1e[wIAnͣ0<=9ꅵUsf7+Ir|aZm( \w4u!|4%0:aGSԼ2R~6,G ϼ׭L[7qj]ʰ-D'k>6t.ͯ [0%w:2+.gJd(-X^dqXJ"k]Nh\`ؿg*{j8Xn}n`mP#q{0µk~g|Fs Gi-B~XWlmغWR?8皎Mzue|500mp*;`OI%;,,%P ɠ:H`u꿖&yOenRzZRcg&Lv4C SN4' x2^:Vb~S81 V21ѮDl7{Oܚ#2vGz**}7oz2E^5 ݧm`פ~w!.0GꉶdLj>?i8?D@|bUȴQb vfWP,#)dgnN/l{K ׃4ΆzcTŜ)ĶZ7EHI:$sS 14>|\09r>l'YaE/>kVC*MB^{n+Lgm^ZAt0yEkzqS݇<ֽ~nC3 SA}g8#o^5h˜e@y=z4RƎ,ʊh9Ϊ;WQ?)'[݁9>ͬ ùgtG,M'҆0ٜu9hvc琞. Js9nv]V3~M{_X<8c嶁0. ?3͹n;JI*>{-ũaPV;y6뙝-g`ۆ} Rő=p41Jt'A4Άz?yjJlXڃaU33CL=0S= 0 and y + ny >= 0: skip = self.remove[y + ny][x + nx] block = self.playing_field[y + ny][x + nx] if skip or block != previous_block: # Block already scheduled for deletion return chain # Add block to the current chain chain += [(x + nx, y + ny)] chain = self.get_chain(chain, x + nx, y + ny, direction, block) return chain def update_field(self): directions = [ (0, -1), (1, -1), (1, 0), (1, 1) ] remove = [] for y in range(17): for x in range(7): skip = self.remove[y][x] block = self.playing_field[y][x] if skip or block == (0, 0, 0): # Block is scheduled for deletion continue for d in directions: chain = [(x, y)] chain = self.get_chain(chain, x, y, d, block) if len(chain) >= 3: remove += chain for x, y in remove: self.remove[y][x] = 1 self.playing_field[y][x] = (128, 128, 128) def remove_blocks(self): for y in range(17): for x in range(7): remove = self.remove[y][x] block = self.playing_field[y][x] if remove: new_block = [max(0, int(c * 0.8)) for c in block] new_block = [0 if c < 5 else c for c in new_block] self.playing_field[y][x] = tuple(new_block) if tuple(new_block) == (0, 0, 0): self.remove[y][x] = 0 # Removal done drop = y while drop > 0: self.playing_field[drop][x] = self.playing_field[drop - 1][x] drop -= 1 def move(self): self._move = True def rotate(self): self._rotate = True class Column: def __init__(self, x, y): self.colors = [ (255, 30, 30), (30, 255, 30), (30, 30, 255) ] self.x = x self.y = y self.sequence = [ random.choice(self.colors), random.choice(self.colors), random.choice(self.colors), ] def rotate(self): self.sequence.insert(0, self.sequence.pop(2)) def move(self, field, x=0, y=0): field_h = len(field) field_w = len(field[0]) if x > 0: new_x = int(self.x + x) % 7 collissions = [col[new_x] for col in field[int(self.y):int(self.y) + 3]] for block in collissions: if block != (0, 0, 0): return True self.x = new_x if y > 0: new_y = self.y + y if int(new_y + 2) >= field_h: return True if field[int(new_y) + 2][self.x] != (0, 0, 0): return True self.y = new_y return False def draw(self, display): for i, color in enumerate(self.sequence): if int(self.y) + i >= 0: display.set_pixel(self.x, int(self.y) + i, *color) def bake(self, field): for i, color in enumerate(self.sequence): field[int(self.y) + i][self.x] = color button_a = Button(5) # Red button_b = Button(6) # [B]lue button_x = Button(16) # Green button_y = Button(24) # [Y]ellow game = Game(unicornhatmini) button_y.when_pressed = game.rotate button_x.when_pressed = game.move while True: game.update() time.sleep(1.0 / 50.0) ./examples/5x7.ttf0000664000175000017500000004604414223641750015135 0ustar jawn-smithjawn-smith0OS/2xoKXNcmapBcvt Y.fpgm3Oglyf,:hdmxll4Dhead4K6hheaaK$hmtx?@Dloca L>maxp L namecpost A AprepvX>n7 D Hb 7"$> n7    D   Hb  1999-2003 / yuji oshimoto / 04@dsg4.com / www.04.jp.org1999 2003 / yuji oshimoo / 04@dsg4.com / www.04.jp.org04b0304b03RegularRegular04b0304b03Macromedia Fontographer 4.1J 04b03Macromedia Fonographer 4.1J 04b03Macromedia Fontographer 4.1J 03.3.25Macromedia Fonographer 4.1J 03.3.2504b0304b03@,vE %E#ah#h`D-q}}wwZgҸj*a@Ҁ:'U @ EhDEhDEhDEhDEhDEhDF+F+EhDEhD?V@ @ Fv/7?@       Fv/7?bl0 x | , !b!"#f$$%j%&'''(j()***+~,",-d-.f//r0 01*12j234@45p6467828929::?qwwwqwqqqqqqqqqqqqqqqqqqqqqqqqqqwwqwqqqqqqwqqqqqqqqqqq{c  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a$$  !"#$%&'()*+,-./0123456789:;<=>?@ABDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a Cb~    !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abh                                                                                                                                                                                                                                       &2Alts@ R_<xxq>qcc @ `./examples/image.py0000775000175000017500000000137514223641750015430 0ustar jawn-smithjawn-smith#!/usr/bin/env python3 import time import sys from PIL import Image from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() rotation = 0 if len(sys.argv) > 1: try: rotation = int(sys.argv[1]) except ValueError: print("Usage: {} ".format(sys.argv[0])) sys.exit(1) unicornhatmini.set_rotation(rotation) display_width, display_height = unicornhatmini.get_shape() print("{}x{}".format(display_width, display_height)) # Do not look at unicornhatmini with remaining eye unicornhatmini.set_brightness(0.1) image = Image.open("twister.png") offset_y = 0 while True: unicornhatmini.set_image(image, offset_y=offset_y, wrap=True) offset_y += 1 unicornhatmini.show() time.sleep(0.01) ./examples/simon.py0000775000175000017500000002640314223641750015472 0ustar jawn-smithjawn-smith#!/usr/bin/env python3 import time import math import random import colorsys from gpiozero import Button from unicornhatmini import UnicornHATMini unicornhatmini = UnicornHATMini() unicornhatmini.set_brightness(0.5) # Digits as 3x5 pixel elements stored as 15bits # MSB is top-left, each 5 bits are a column digits_5x3 = [ 0b111111000111111, # 0 0b100011111100001, # 1 0b101111010111101, # 2 0b101011010111111, # 3 0b111000010011111, # 4 0b111011010110111, # 5 0b111111010100111, # 6 0b100001000011111, # 7 0b111111010111111, # 8 0b111001010011111 # 9 ] R = 0 G = 1 B = 2 Y = 3 class Display(): """Virtual Simon display class. This class wraps an output device (unicornhatmini) and makes it behave like a display with four fixed colour lights (Red, Yellow, Blue and Green) and two 3x5 numeric digits. """ def __init__(self, output_device): self._output = output_device self._width, self._height = self._output.get_shape() self._br_red = 0 self._br_green = 0 self._br_blue = 0 self._br_yellow = 0 self._level = 0 self.red = (255, 0, 0) self.green = (0, 255, 0) self.blue = (0, 0, 255) self.yellow = (255, 255, 0) self._digit_left = None self._digit_right = None self._digit_left_br = 1.0 self._digit_right_br = 1.0 self._digit_left_color = (128, 128, 128) self._digit_right_color = (128, 128, 128) def _draw_light(self, brightness, x, y, r, g, b): r, g, b = [int(c * brightness) for c in (r, g, b)] self._draw_rect(x, y, 3, 3, r, g, b) def _draw_rect(self, x, y, w, h, r, g, b): for ry in range(h): for rx in range(w): self._output.set_pixel(x + rx, y + ry, r, g, b) def _draw_digit(self, digit, x, y, r, g, b): digit = digits_5x3[digit] cols = [ (digit >> 10) & 0b11111, (digit >> 5) & 0b11111, (digit) & 0b11111 ] for dx in range(3): col = cols[dx] for dy in range(5): if col & (1 << (4 - dy)): self._output.set_pixel(x + dx, y + dy, r, g, b) def clear(self): self._output.clear() def update(self): self._draw_light(self._br_red, 0, 0, *self.red) self._draw_light(self._br_blue, 0, self._height - 3, *self.green) self._draw_light(self._br_green, self._width - 3, 0, *self.blue) self._draw_light(self._br_yellow, self._width - 3, self._height - 3, *self.yellow) # Draw the current digts (score/level/lives, kinda) if self._digit_left is not None: r, g, b = [int(c * self._digit_left_br) for c in self._digit_left_color] self._draw_digit(self._digit_left, 5, 1, r, g, b) if self._digit_right is not None: r, g, b = [int(c * self._digit_right_br) for c in self._digit_right_color] self._draw_digit(self._digit_right, 9, 1, r, g, b) self._output.show() def set_light_brightness(self, red, green, blue, yellow): self._br_red = red self._br_green = green self._br_blue = blue self._br_yellow = yellow def set_digits(self, left, right): self._digit_left = left self._digit_right = right def set_digit_brightness(self, left, right): self._digit_left_br = left self._digit_right_br = right def set_digit_color(self, left, right): self._digit_left_color = left self._digit_right_color = right class Game(): def __init__(self, display, starting_lives=3, starting_level=0, mode='attract'): self._starting_lives = starting_lives self._starting_level = starting_level self._mode = mode self._display = display self._level = 0 self._lives = 0 self._sequence = [] self._compare = [] self._current_playback_step = 0 self._current_mode_start = 0 self._button_map = {'a': R, 'b': B, 'x': G, 'y': Y} def update(self): self._display.clear() getattr(self, "_{}".format(self._mode))(time.time()) self._display.update() def _set_mode(self, mode): self._mode = mode self._current_mode_start = time.time() def _attract(self, time): """Mode: Attract. Pulses all the virtual lights and fades the numbers "51" ([si]mon) on the virtual digits. """ self._display.set_digits(5, 1) self._display.set_light_brightness( self._pulse(time / 2), self._pulse((time + 0.25) / 2), self._pulse((time + 0.5) / 2), self._pulse((time + 0.75) / 2) ) self._display.set_digit_brightness( self._pulse(time), self._pulse(time) ) self._display.set_digit_color( self._hue(time / 10), self._hue(time / 10 + 1) ) def _play_pattern(self, time): """Mode: Play Pattern. Steps through the current sequence and pulses each virtual light. Drops into "wait_for_input" mode when it's finished, this is determined by checking if the elapsed time is greater than the sequence length- since each "light" takes 1 second to pulse. """ self._display_level((255, 0, 0)) br = [0, 0, 0, 0] color = self._sequence[self._current_playback_step] br[color] = self._pulse(time - self._current_mode_start) self._display.set_light_brightness(*br) if time - self._current_mode_start > (self._current_playback_step + 1): self._current_playback_step += 1 if self._current_playback_step >= len(self._sequence): self._current_playback_step = 0 self._set_mode('wait_for_input') def _flash_lives(self, time): """Mode: Flash Lives. Flash the players lives count at them. Re-play the pattern if they have lives left, otherwise jump to "you_lose" """ # To show the player that they've lost one life we flash # the lives count up in red. # We fake add one life to their count (which has been already deducted) # for half of the flash cycle, so they visibly see the count go down! fake_lives = self._lives if time - self._current_mode_start < 1.5: fake_lives += 1 self._display.set_digits(int(fake_lives / 10), fake_lives % 10) self._display.set_digit_brightness( self._pulse(time), self._pulse(time) ) self._display.set_digit_color((255, 0, 0), (255, 0, 0)) # Flash lives for 3 seconds and then switch back to playing pattern if time - self._current_mode_start > 3.0: if self._lives > 0: self._set_mode('play_pattern') else: self._set_mode('you_lose') def _you_win(self, time): """Need a better win animation!""" self._display_level() self._display.set_light_brightness( self._pulse(time), self._pulse(time), self._pulse(time), self._pulse(time) ) def _you_lose(self, time): """Flash the player's 0 lives at them in red!""" self._display.set_digits(0, 0) self._display.set_digit_brightness( self._pulse(time), self._pulse(time) ) self._display.set_digit_color((255, 0, 0), (255, 0, 0)) # Return back to attract mode after displaying losing zeros for 20s if time - self._current_mode_start > 20.0: self._set_mode('attract') def _wait_for_input(self, time): """Just display the current level and wait for the players input. This constantly checks the players input against the sequence, if it ever mismatches it deducts one life and switches to "flash_lives". A successful sequence match will call "next_level()" """ self._display_level() if self._compare == self._sequence[:len(self._compare)]: if len(self._compare) == len(self._sequence): self.next_level() else: # Remove the last (incorrect) guess self._compare = [] self._lives -= 1 self._set_mode('flash_lives') def _display_level(self, color=(255, 255, 255)): """Helper to display the current level on the virtual digits.""" self._display.set_digit_brightness(0.5, 0.5) self._display.set_digit_color(color, color) self._display.set_digits( int(self._level / 10.0), self._level % 10 ) def _pulse(self, time): """Helper to produce a sine wave with a period of 1sec""" return (math.sin(time * 2 * math.pi - (math.pi / 2)) + 1) / 2.0 def _hue(self, h): """Helper to return an RGB colour from HSV""" return tuple([int(c * 255) for c in colorsys.hsv_to_rgb(h, 1.0, 1.0)]) def start(self): """Start the game. Sets the level to the starting level and builds a long-enough sequence to begin. """ self._lives = self._starting_lives self._level = self._starting_level self._compare = [] self._sequence = [random.choice([R, G, B, Y])] * (self._level + 1) self._current_playback_step = 0 self._set_mode('play_pattern') def next_level(self): """Proceed to the next level. Adds 1 to the current level and a new random item to the end of the sequence. Jumps to the win state if the level hits 100! """ self._level += 1 self._compare = [] if self._level == 100: self._set_mode('you_win') return self._sequence += [random.choice([R, G, B, Y])] self._set_mode('play_pattern') def _handle_choice(self, button): """Handle a specific choice.""" self._compare += [self._button_map[button]] def _handle_generic_input(self): """Handle user input that's not button-specific. This function should be called for any button if it has not otherwise been handled. It's responsible for starting the game, mostly. """ if self._mode in ('attract', 'you_lose', 'you_win'): self.start() def button_a(self): if self._mode == 'wait_for_input': self._handle_choice('a') return self._handle_generic_input() def button_b(self): if self._mode == 'wait_for_input': self._handle_choice('b') return self._handle_generic_input() def button_x(self): if self._mode == 'wait_for_input': self._handle_choice('x') return self._handle_generic_input() def button_y(self): if self._mode == 'wait_for_input': self._handle_choice('y') return self._handle_generic_input() button_a = Button(5) # Red button_b = Button(6) # [B]lue button_x = Button(16) # Green button_y = Button(24) # [Y]ellow display = Display(output_device=unicornhatmini) game = Game(display) button_a.when_pressed = game.button_a button_b.when_pressed = game.button_b button_x.when_pressed = game.button_x button_y.when_pressed = game.button_y while True: game.update() time.sleep(1.0 / 50) ./examples/colour-cycle.py0000775000175000017500000000100514223641750016734 0ustar jawn-smithjawn-smith#!/usr/bin/env python3 import time from colorsys import hsv_to_rgb from unicornhatmini import UnicornHATMini print("""Unicorn HAT Mini: colour-cycle.py Cycles through colour hues across all of Unicorn HAT Mini's pixels. Press Ctrl+C to exit! """) unicornhatmini = UnicornHATMini() unicornhatmini.set_brightness(0.1) while True: hue = (time.time() / 10.0) r, g, b = [int(c * 255) for c in hsv_to_rgb(hue, 1.0, 1.0)] unicornhatmini.set_all(r, g, b) unicornhatmini.show() time.sleep(1.0 / 60) ./examples/rainbow.py0000775000175000017500000000166714223641750016013 0ustar jawn-smithjawn-smith#!/usr/bin/env python3 import time import math from colorsys import hsv_to_rgb from unicornhatmini import UnicornHATMini print("""Unicorn HAT Mini: rainbow.py Displays a concentric rainbow that moves around the Unicorn HAT Mini display. Press Ctrl+C to exit! """) unicornhatmini = UnicornHATMini() unicornhatmini.set_brightness(0.1) unicornhatmini.set_rotation(0) width, height = unicornhatmini.get_shape() step = 0 while True: step += 1 for x in range(0, width): for y in range(0, height): dx = (math.sin(step / width + 20) * width) + height dy = (math.cos(step / height) * height) + height sc = (math.cos(step / height) * height) + width hue = math.sqrt(math.pow(x - dx, 2) + math.pow(y - dy, 2)) / sc r, g, b = [int(c * 255) for c in hsv_to_rgb(hue, 1, 1)] unicornhatmini.set_pixel(x, y, r, g, b) unicornhatmini.show() time.sleep(1.0 / 60) ./examples/fps.py0000775000175000017500000000210014223641750015121 0ustar jawn-smithjawn-smith#!/usr/bin/env python3 import time from colorsys import hsv_to_rgb from unicornhatmini import UnicornHATMini print("""Unicorn HAT Mini: fps.py Attempts to refresh the Unicorn HAT Mini display as fast as possible with a horizontal rainbow and displays the frames per second refresh rate. Press Ctrl+C to exit! """) unicornhatmini = UnicornHATMini() unicornhatmini.set_brightness(0.1) unicornhatmini.set_rotation(0) width, height = unicornhatmini.get_shape() frames = 0 t_start = time.time() t_report = time.time() report_freq = 5.0 print("Please wait...") while True: for y in range(height): for x in range(width): hue = (time.time() / 10.0) + (x / float(width * 2)) r, g, b = [int(c * 255) for c in hsv_to_rgb(hue, 1.0, 1.0)] unicornhatmini.set_pixel(x, y, r, g, b) unicornhatmini.show() frames += 1 if time.time() - t_report > report_freq: t_report = time.time() t = time.time() - t_start fps = frames / t print("FPS: {:05.3f} ({} frames in {:.1f} seconds)".format(fps, frames, t))