├── .github └── workflows │ └── test.yml ├── .gitignore ├── .stickler.yml ├── LICENSE ├── Makefile ├── README.md ├── examples ├── pong.py ├── pwm-backlight.py ├── pygame-basic.py ├── pygame-button-interrupt.py └── pygame-demo.py ├── install-bullseye.sh ├── install.sh ├── library ├── .coveragerc ├── CHANGELOG.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── displayhatmini │ ├── __init__.py │ └── __main__.py ├── pyproject.toml ├── setup.cfg ├── setup.py ├── tests │ ├── conftest.py │ └── test_setup.py └── tox.ini └── uninstall.sh /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Python Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python: [2.7, 3.5, 3.7, 3.9] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python }} 22 | - name: Install Dependencies 23 | run: | 24 | python -m pip install --upgrade setuptools tox 25 | - name: Run Tests 26 | working-directory: library 27 | run: | 28 | tox -e py 29 | - name: Coverage 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | working-directory: library 33 | run: | 34 | python -m pip install coveralls 35 | coveralls --service=github 36 | if: ${{ matrix.python == '3.9' }} 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | _build/ 3 | *.o 4 | *.so 5 | *.a 6 | *.py[cod] 7 | *.egg-info 8 | dist/ 9 | __pycache__ 10 | .DS_Store 11 | *.deb 12 | *.dsc 13 | *.build 14 | *.changes 15 | *.orig.* 16 | packaging/*tar.xz 17 | library/debian/ 18 | .coverage 19 | .pytest_cache 20 | .tox 21 | -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | linters: 3 | flake8: 4 | python: 3 5 | max-line-length: 160 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pimoroni Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LIBRARY_VERSION=$(shell grep version library/setup.cfg | awk -F" = " '{print $$2}') 2 | LIBRARY_NAME=$(shell grep name library/setup.cfg | awk -F" = " '{print $$2}') 3 | 4 | .PHONY: usage install uninstall 5 | usage: 6 | @echo "Library: ${LIBRARY_NAME}" 7 | @echo "Version: ${LIBRARY_VERSION}\n" 8 | @echo "Usage: make , where target is one of:\n" 9 | @echo "install: install the library locally from source" 10 | @echo "uninstall: uninstall the local library" 11 | @echo "check: peform basic integrity checks on the codebase" 12 | @echo "python-readme: generate library/README.md from README.md + library/CHANGELOG.txt" 13 | @echo "python-wheels: build python .whl files for distribution" 14 | @echo "python-sdist: build python source distribution" 15 | @echo "python-clean: clean python build and dist directories" 16 | @echo "python-dist: build all python distribution files" 17 | @echo "python-testdeploy: build all and deploy to test PyPi" 18 | @echo "tag: tag the repository with the current version" 19 | 20 | install: 21 | ./install.sh 22 | 23 | uninstall: 24 | ./uninstall.sh 25 | 26 | check: 27 | @echo "Checking for trailing whitespace" 28 | @! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO 29 | @echo "Checking for DOS line-endings" 30 | @! grep -lIUrn --color " " --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile 31 | @echo "Checking library/CHANGELOG.txt" 32 | @cat library/CHANGELOG.txt | grep ^${LIBRARY_VERSION} 33 | @echo "Checking library/${LIBRARY_NAME}/__init__.py" 34 | @cat library/${LIBRARY_NAME}/__init__.py | grep "^__version__ = '${LIBRARY_VERSION}'" 35 | 36 | tag: 37 | git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" 38 | 39 | python-readme: library/README.md 40 | 41 | python-license: library/LICENSE.txt 42 | 43 | library/README.md: README.md library/CHANGELOG.txt 44 | cp README.md library/README.md 45 | printf "\n# Changelog\n" >> library/README.md 46 | cat library/CHANGELOG.txt >> library/README.md 47 | 48 | library/LICENSE.txt: LICENSE 49 | cp LICENSE library/LICENSE.txt 50 | 51 | python-wheels: python-readme python-license 52 | cd library; python3 setup.py bdist_wheel 53 | #cd library; python setup.py bdist_wheel 54 | 55 | python-sdist: python-readme python-license 56 | cd library; python3 setup.py sdist 57 | 58 | python-clean: 59 | -rm -r library/dist 60 | -rm -r library/build 61 | -rm -r library/*.egg-info 62 | 63 | python-dist: python-clean python-wheels python-sdist 64 | ls library/dist 65 | 66 | python-testdeploy: python-dist 67 | twine upload --repository-url https://test.pypi.org/legacy/ library/dist/* 68 | 69 | python-deploy: check python-dist 70 | twine upload library/dist/* 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Display HAT Mini 2 | 3 | [![Build Status](https://shields.io/github/workflow/status/pimoroni/displayhatmini-python/Python%20Tests.svg)](https://github.com/pimoroni/displayhatmini-python/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/displayhatmini-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/displayhatmini-python?branch=main) 5 | [![PyPi Package](https://img.shields.io/pypi/v/displayhatmini.svg)](https://pypi.python.org/pypi/displayhatmini) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/displayhatmini.svg)](https://pypi.python.org/pypi/displayhatmini) 7 | 8 | # Pre-requisites 9 | 10 | You must enable: 11 | 12 | * spi: `sudo raspi-config nonint do_spi 0` 13 | 14 | You can optionally run `sudo raspi-config` or the graphical Raspberry Pi Configuration UI to enable interfaces. 15 | 16 | # Installing 17 | 18 | Stable library from PyPi: 19 | 20 | * Just run `pip3 install displayhatmini` 21 | 22 | In some cases you may need to use `sudo` or install pip with: `sudo apt install python3-pip` 23 | 24 | Latest/development library from GitHub: 25 | 26 | * `git clone https://github.com/pimoroni/displayhatmini-python` 27 | * `cd displayhatmini-python` 28 | * `sudo ./install.sh` 29 | 30 | # Useful Links 31 | 32 | * Pinout - https://pinout.xyz/pinout/display_hat_mini 33 | * Buy - https://shop.pimoroni.com/products/display-hat-mini -------------------------------------------------------------------------------- /examples/pong.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import random 3 | import time 4 | import math 5 | from displayhatmini import DisplayHATMini 6 | from collections import namedtuple 7 | from turtle import Vec2D 8 | 9 | try: 10 | from PIL import Image, ImageDraw, ImageFont 11 | except ImportError: 12 | print("""This example requires PIL/Pillow, try: 13 | 14 | sudo apt install python3-pil 15 | 16 | """) 17 | 18 | width = DisplayHATMini.WIDTH 19 | height = DisplayHATMini.HEIGHT 20 | buffer = Image.new("RGB", (width, height)) 21 | draw = ImageDraw.Draw(buffer) 22 | 23 | displayhatmini = DisplayHATMini(buffer) 24 | displayhatmini.set_led(0.05, 0.05, 0.05) 25 | 26 | 27 | Position = namedtuple('Position', 'x y') 28 | Size = namedtuple('Size', 'w h') 29 | 30 | 31 | def millis(): 32 | return int(round(time.time() * 1000)) 33 | 34 | 35 | def text(draw, text, position, size, color): 36 | fnt = ImageFont.load_default() 37 | draw.text(position, text, font=fnt, fill=color) 38 | 39 | 40 | class Ball(): 41 | def __init__(self): 42 | global width, height 43 | self.position = Vec2D(width / 2, height / 2) 44 | self.velocity = Vec2D(0.15, 0.15) 45 | self.radius = 5 46 | self.color = (255, 255, 255) 47 | 48 | def reset(self): 49 | self.velocity = Vec2D(0.15, 0.15).rotate(random.randint(0, 360)) 50 | self.position = Vec2D(width / 2, height / 2) 51 | 52 | @property 53 | def x(self): 54 | return self.position[0] 55 | 56 | @x.setter 57 | def x(self, value): 58 | self.position = Vec2D(value, self.position[1]) 59 | 60 | @property 61 | def y(self): 62 | return self.position[1] 63 | 64 | @y.setter 65 | def y(self, value): 66 | self.position = Vec2D(self.position[0], value) 67 | 68 | @property 69 | def vx(self): 70 | return self.velocity[0] 71 | 72 | @vx.setter 73 | def vx(self, value): 74 | self.velocity = Vec2D(value, self.velocity[1]) 75 | 76 | @property 77 | def vy(self): 78 | return self.velocity[1] 79 | 80 | @vy.setter 81 | def vy(self, value): 82 | self.velocity = Vec2D(self.velocity[0], value) 83 | 84 | @property 85 | def speed(self): 86 | return abs(self.velocity) 87 | 88 | def intersects(self, rect): 89 | rx, ry = rect.center 90 | rw, rh = rect.size 91 | 92 | dist_x = abs(self.x - rx) 93 | dist_y = abs(self.y - ry) 94 | 95 | if dist_x > rw / 2.0 + self.radius or dist_y > rh / 2.0 + self.radius: 96 | return False 97 | 98 | if dist_x <= rw / 2.0 or dist_y <= rh / 2.0: 99 | return True 100 | 101 | cx = dist_x - rw / 2.0 102 | cy = dist_y - rh / 2.0 103 | 104 | c_sq = cx ** 2.0 + cy ** 2.0 105 | 106 | return c_sq <= self.radius ** 2.0 107 | 108 | def update(self, delta, left_player, right_player): 109 | global width 110 | 111 | self.position += self.velocity * delta 112 | 113 | if (self.x < 50 and self.vx < 0) or (self.x > width - 50 and self.vx > 0): 114 | for item in [left_player, right_player]: 115 | if self.intersects(item): 116 | item.success() 117 | 118 | cx, cy = item.center 119 | w, h = item.size 120 | relative_y = (cy - self.y) / (h / 2) 121 | 122 | speed = self.speed + (abs(relative_y) / 4) 123 | 124 | angle = relative_y * 5 * (math.pi / 12) 125 | 126 | if self.x > width / 2: 127 | self.x = item.position.x - self.radius 128 | self.velocity = Vec2D( 129 | speed * -math.cos(angle), 130 | speed * -math.sin(angle)) 131 | else: 132 | self.x = item.position.x + item.width + self.radius 133 | self.velocity = Vec2D( 134 | speed * math.cos(angle), 135 | speed * -math.sin(angle)) 136 | 137 | if self.x - self.radius < 0 and self.vx < 0: 138 | left_player.fail() 139 | self.reset() 140 | 141 | elif self.x + self.radius > width and self.vx > 0: 142 | right_player.fail() 143 | self.reset() 144 | 145 | if self.y - self.radius < 0 and self.vy < 0: 146 | self.y = self.radius 147 | self.vy *= -1 148 | 149 | elif self.y + self.radius > height and self.vy > 0: 150 | self.y = height - self.radius 151 | self.vy *= -1 152 | 153 | def render(self, screen): 154 | draw.ellipse(( 155 | self.x - self.radius, 156 | self.y - self.radius, 157 | self.x + self.radius, 158 | self.y + self.radius), 159 | self.color) 160 | 161 | 162 | class Player(): 163 | def __init__(self, side): 164 | global width, height 165 | 166 | self.score = 0 167 | self.y = height / 2 168 | self.next_y = self.y 169 | 170 | if side == 0: # Left 171 | self.x = 25 172 | else: 173 | self.x = width - 25 174 | 175 | self.width = 5 176 | self.height = 50 177 | 178 | def paddle(self, y): 179 | self.next_y = y 180 | 181 | def success(self): 182 | self.score += 1 183 | 184 | def fail(self): 185 | self.score -= 1 186 | 187 | @property 188 | def center(self): 189 | return Position( 190 | x=self.x, 191 | y=self.y) 192 | 193 | @property 194 | def position(self): 195 | return Position( 196 | x=self.x - (self.width / 2), 197 | y=self.y - (self.height / 2)) 198 | 199 | @property 200 | def size(self): 201 | return Size( 202 | w=self.width, 203 | h=self.height) 204 | 205 | def update(self): 206 | self.y = self.next_y 207 | 208 | def render(self, draw): 209 | draw.rectangle((self.x - (self.width / 2), 210 | self.y - (self.height / 2), 211 | self.x + (self.width / 2), 212 | self.y + (self.height / 2)), (255, 255, 255)) 213 | 214 | 215 | player_one = Player(0) 216 | player_two = Player(1) 217 | ball = Ball() 218 | 219 | time_last = millis() 220 | 221 | player_one_pos = height / 2 222 | player_two_pos = height / 2 223 | 224 | displayhatmini.set_led(0, 0, 0) 225 | 226 | paddle_speed = 15 227 | 228 | while True: 229 | time_now = millis() 230 | time_delta = time_now - time_last 231 | 232 | player_one_pos = player_one.center.y 233 | if displayhatmini.read_button(displayhatmini.BUTTON_A): 234 | player_one_pos -= paddle_speed 235 | if displayhatmini.read_button(displayhatmini.BUTTON_B): 236 | player_one_pos += paddle_speed 237 | 238 | player_two_pos = player_two.center.y 239 | if displayhatmini.read_button(displayhatmini.BUTTON_X): 240 | player_two_pos -= paddle_speed 241 | if displayhatmini.read_button(displayhatmini.BUTTON_Y): 242 | player_two_pos += paddle_speed 243 | 244 | if player_one_pos < 0: 245 | player_one_pos = 0 246 | elif player_one_pos > height: 247 | player_one_pos = height 248 | 249 | if player_two_pos < 0: 250 | player_two_pos = 0 251 | elif player_two_pos > height: 252 | player_two_pos = height 253 | 254 | player_one.paddle(player_one_pos) 255 | player_two.paddle(player_two_pos) 256 | 257 | draw.rectangle((0, 0, width, height), (0, 0, 0)) 258 | 259 | draw.rectangle(( 260 | (width / 2) - 1, 261 | 20, 262 | (width / 2) + 1, 263 | height - 20), 264 | (64, 64, 64)) 265 | 266 | text(draw, "{0:02d}".format(player_one.score), (25, 25), 15, (255, 255, 255)) 267 | text(draw, "{0:02d}".format(player_two.score), (width - 25, 25), 15, (255, 255, 255)) 268 | 269 | player_one.update() 270 | player_two.update() 271 | ball.update(time_delta, player_one, player_two) 272 | 273 | ball.render(draw) 274 | player_one.render(draw) 275 | player_two.render(draw) 276 | 277 | displayhatmini.display() 278 | 279 | time.sleep(0.001) 280 | time_last = time_now 281 | -------------------------------------------------------------------------------- /examples/pwm-backlight.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import time 3 | from displayhatmini import DisplayHATMini 4 | 5 | try: 6 | from PIL import Image, ImageDraw, ImageFont 7 | except ImportError: 8 | print("""This example requires PIL/Pillow, try: 9 | 10 | sudo apt install python3-pil 11 | 12 | """) 13 | 14 | width = DisplayHATMini.WIDTH 15 | height = DisplayHATMini.HEIGHT 16 | buffer = Image.new("RGB", (width, height)) 17 | draw = ImageDraw.Draw(buffer) 18 | font = ImageFont.load_default() 19 | 20 | displayhatmini = DisplayHATMini(buffer, backlight_pwm=True) 21 | displayhatmini.set_led(0.05, 0.05, 0.05) 22 | 23 | brightness = 1.0 24 | 25 | 26 | # Plumbing to convert Display HAT Mini button presses into pygame events 27 | def button_callback(pin): 28 | global brightness 29 | 30 | # Only handle presses 31 | if not displayhatmini.read_button(pin): 32 | return 33 | 34 | if pin == displayhatmini.BUTTON_A: 35 | brightness += 0.1 36 | brightness = min(1, brightness) 37 | 38 | if pin == displayhatmini.BUTTON_B: 39 | brightness -= 0.1 40 | brightness = max(0, brightness) 41 | 42 | 43 | displayhatmini.on_button_pressed(button_callback) 44 | 45 | draw.rectangle((0, 0, width, height), (255, 255, 255)) 46 | draw.text((10, 70), "Backlight Up", font=font, fill=(0, 0, 0)) 47 | draw.text((10, 160), "Backlight Down", font=font, fill=(0, 0, 0)) 48 | 49 | while True: 50 | displayhatmini.display() 51 | displayhatmini.set_backlight(brightness) 52 | time.sleep(1.0 / 30) 53 | -------------------------------------------------------------------------------- /examples/pygame-basic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import signal 5 | import pygame 6 | import time 7 | import math 8 | 9 | from displayhatmini import DisplayHATMini 10 | 11 | print("""Display HAT Mini: Basic Pygame Demo""") 12 | 13 | if pygame.vernum < (2, 0, 0): 14 | print("Need PyGame >= 2.0.0:\n python3 -m pip install pygame --upgrade") 15 | sys.exit(1) 16 | 17 | 18 | def _exit(sig, frame): 19 | global running 20 | running = False 21 | print("\nExiting!...\n") 22 | 23 | 24 | def update_display(): 25 | display_hat.st7789.set_window() 26 | # Grab the pygame screen as a bytes object 27 | pixelbytes = pygame.transform.rotate(screen, 180).convert(16, 0).get_buffer() 28 | # Lazy (slow) byteswap: 29 | pixelbytes = bytearray(pixelbytes) 30 | pixelbytes[0::2], pixelbytes[1::2] = pixelbytes[1::2], pixelbytes[0::2] 31 | # Bypass the ST7789 PIL image RGB888->RGB565 conversion 32 | for i in range(0, len(pixelbytes), 4096): 33 | display_hat.st7789.data(pixelbytes[i:i + 4096]) 34 | 35 | 36 | display_hat = DisplayHATMini(None) 37 | 38 | os.putenv('SDL_VIDEODRIVER', 'dummy') 39 | pygame.display.init() # Need to init for .convert() to work 40 | screen = pygame.Surface((display_hat.WIDTH, display_hat.HEIGHT)) 41 | 42 | signal.signal(signal.SIGINT, _exit) 43 | 44 | running = True 45 | 46 | while running: 47 | for event in pygame.event.get(): 48 | if event.type == pygame.QUIT: 49 | running = False 50 | break 51 | if event.type == pygame.KEYDOWN: 52 | if event.key == pygame.K_ESCAPE: 53 | running = False 54 | break 55 | 56 | # Clear the screen 57 | screen.fill((0, 0, 0)) 58 | 59 | box_w = display_hat.WIDTH // 3 60 | box_h = display_hat.HEIGHT // 2 61 | 62 | pygame.draw.rect(screen, (255, 0, 0), (0, 0, box_w, box_h)) 63 | pygame.draw.rect(screen, (0, 255, 0), (box_w, 0, box_w, box_h)) 64 | pygame.draw.rect(screen, (0, 0, 255), (box_w * 2, 0, box_w, box_h)) 65 | 66 | pygame.draw.rect(screen, (255, 255, 0), (0, box_h, box_w, box_h)) 67 | pygame.draw.rect(screen, (255, 0, 255), (box_w, box_h, box_w, box_h)) 68 | pygame.draw.rect(screen, (0, 255, 255), (box_w * 2, box_h, box_w, box_h)) 69 | 70 | r = 50 71 | x = math.sin(time.time() * 2) * (display_hat.WIDTH - r) / 2 72 | y = math.cos(time.time()) * (display_hat.HEIGHT - r) / 2 73 | x += display_hat.WIDTH // 2 74 | y += display_hat.HEIGHT // 2 75 | pygame.draw.circle(screen, (0, 0, 0), (int(x), int(y)), r) 76 | 77 | update_display() 78 | 79 | 80 | pygame.quit() 81 | sys.exit(0) 82 | -------------------------------------------------------------------------------- /examples/pygame-button-interrupt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import signal 5 | import pygame 6 | import time 7 | import math 8 | from threading import Lock 9 | 10 | from displayhatmini import DisplayHATMini 11 | 12 | 13 | print("""Display HAT Mini: Basic Pygame Demo""") 14 | 15 | if pygame.vernum < (2, 0, 0): 16 | print("Need PyGame >= 2.0.0:\n python3 -m pip install pygame --upgrade") 17 | sys.exit(1) 18 | 19 | 20 | def _exit(sig, frame): 21 | global running 22 | running = False 23 | print("\nExiting!...\n") 24 | 25 | 26 | def update_display(): 27 | display_hat.st7789.set_window() 28 | # Grab the pygame screen as a bytes object 29 | pixelbytes = pygame.transform.rotate(screen, 180).convert(16, 0).get_buffer() 30 | # Lazy (slow) byteswap: 31 | pixelbytes = bytearray(pixelbytes) 32 | pixelbytes[0::2], pixelbytes[1::2] = pixelbytes[1::2], pixelbytes[0::2] 33 | # Bypass the ST7789 PIL image RGB888->RGB565 conversion 34 | for i in range(0, len(pixelbytes), 4096): 35 | display_hat.st7789.data(pixelbytes[i:i + 4096]) 36 | 37 | 38 | display_hat = DisplayHATMini(None) 39 | event_lock = Lock() 40 | 41 | os.putenv('SDL_VIDEODRIVER', 'dummy') 42 | pygame.display.init() # Need to init for .convert() to work 43 | screen = pygame.Surface((display_hat.WIDTH, display_hat.HEIGHT)) 44 | 45 | signal.signal(signal.SIGINT, _exit) 46 | 47 | 48 | # Plumbing to convert Display HAT Mini button presses into pygame events 49 | def button_callback(pin): 50 | key = { 51 | display_hat.BUTTON_A: 'a', 52 | display_hat.BUTTON_B: 'b', 53 | display_hat.BUTTON_X: 'x', 54 | display_hat.BUTTON_Y: 'y' 55 | }[pin] 56 | event = pygame.KEYDOWN if display_hat.read_button(pin) else pygame.KEYUP 57 | pygame.event.post(pygame.event.Event(event, unicode=key, key=pygame.key.key_code(key))) 58 | 59 | 60 | display_hat.on_button_pressed(button_callback) 61 | 62 | running = True 63 | 64 | while running: 65 | for event in pygame.event.get(): 66 | if event.type == pygame.QUIT: 67 | running = False 68 | break 69 | if event.type == pygame.KEYDOWN: 70 | if event.key in (pygame.K_a, pygame.K_ESCAPE): 71 | running = False 72 | break 73 | 74 | # Clear the screen 75 | screen.fill((0, 0, 0)) 76 | 77 | box_w = display_hat.WIDTH // 3 78 | box_h = display_hat.HEIGHT // 2 79 | 80 | pygame.draw.rect(screen, (255, 0, 0), (0, 0, box_w, box_h)) 81 | pygame.draw.rect(screen, (0, 255, 0), (box_w, 0, box_w, box_h)) 82 | pygame.draw.rect(screen, (0, 0, 255), (box_w * 2, 0, box_w, box_h)) 83 | 84 | pygame.draw.rect(screen, (255, 255, 0), (0, box_h, box_w, box_h)) 85 | pygame.draw.rect(screen, (255, 0, 255), (box_w, box_h, box_w, box_h)) 86 | pygame.draw.rect(screen, (0, 255, 255), (box_w * 2, box_h, box_w, box_h)) 87 | 88 | r = 50 89 | x = math.sin(time.time() * 2) * (display_hat.WIDTH - r) / 2 90 | y = math.cos(time.time()) * (display_hat.HEIGHT - r) / 2 91 | x += display_hat.WIDTH // 2 92 | y += display_hat.HEIGHT // 2 93 | pygame.draw.circle(screen, (0, 0, 0), (int(x), int(y)), r) 94 | 95 | update_display() 96 | 97 | 98 | screen.fill((0, 0, 0)) 99 | update_display() 100 | 101 | pygame.quit() 102 | sys.exit(0) -------------------------------------------------------------------------------- /examples/pygame-demo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import signal 5 | import pygame 6 | import time 7 | import colorsys 8 | import math 9 | 10 | from displayhatmini import DisplayHATMini 11 | 12 | 13 | print("""Display HAT Mini: Pygame Demo""") 14 | 15 | if pygame.vernum < (2, 0, 0): 16 | print("Need PyGame >= 2.0.0:\n python3 -m pip install pygame --upgrade") 17 | sys.exit(1) 18 | 19 | 20 | hue_to_rgb = [] 21 | 22 | 23 | for i in range(0, 255): 24 | hue_to_rgb.append(colorsys.hsv_to_rgb(i / 255.0, 1, 1)) 25 | 26 | 27 | # zoom tunnel 28 | def tunnel(x, y, step): 29 | u_width = 32 30 | u_height = 32 31 | speed = step / 100.0 32 | x -= (u_width / 2) 33 | y -= (u_height / 2) 34 | xo = math.sin(step / 27.0) * 2 35 | yo = math.cos(step / 18.0) * 2 36 | x += xo 37 | y += yo 38 | if y == 0: 39 | if x < 0: 40 | angle = -(math.pi / 2) 41 | else: 42 | angle = (math.pi / 2) 43 | else: 44 | angle = math.atan(x / y) 45 | if y > 0: 46 | angle += math.pi 47 | angle /= 2 * math.pi # convert angle to 0...1 range 48 | hyp = math.sqrt(math.pow(x, 2) + math.pow(y, 2)) 49 | shade = hyp / 2.1 50 | shade = 1 if shade > 1 else shade 51 | angle += speed 52 | depth = speed + (hyp / 10) 53 | col1 = hue_to_rgb[step % 255] 54 | col1 = (col1[0] * 0.8, col1[1] * 0.8, col1[2] * 0.8) 55 | col2 = hue_to_rgb[step % 255] 56 | col2 = (col2[0] * 0.3, col2[1] * 0.3, col2[2] * 0.3) 57 | col = col1 if int(abs(angle * 6.0)) % 2 == 0 else col2 58 | td = .3 if int(abs(depth * 3.0)) % 2 == 0 else 0 59 | col = (col[0] + td, col[1] + td, col[2] + td) 60 | col = (col[0] * shade, col[1] * shade, col[2] * shade) 61 | return (col[0] * 255, col[1] * 255, col[2] * 255) 62 | 63 | 64 | class PygameDHM: 65 | screen = None 66 | 67 | def __init__(self): 68 | self.dhm = DisplayHATMini(None) 69 | self._init_display() 70 | 71 | self.screen.fill((0, 0, 0)) 72 | self._updatefb() 73 | 74 | self._running = False 75 | 76 | def _exit(self, sig, frame): 77 | self._running = False 78 | print("\nExiting!...\n") 79 | 80 | def _init_display(self): 81 | os.putenv('SDL_VIDEODRIVER', 'dummy') 82 | pygame.display.init() # Need to init for .convert() to work 83 | self.screen = pygame.Surface((320, 240)) 84 | 85 | def __del__(self): 86 | "Destructor to make sure pygame shuts down, etc." 87 | 88 | def _updatefb(self): 89 | self.dhm.st7789.set_window() 90 | # Grab the pygame screen as a bytes object 91 | pixelbytes = pygame.transform.rotate(self.screen, 180).convert(16, 0).get_buffer() 92 | # Lazy (slow) byteswap: 93 | pixelbytes = bytearray(pixelbytes) 94 | pixelbytes[0::2], pixelbytes[1::2] = pixelbytes[1::2], pixelbytes[0::2] 95 | # Bypass the ST7789 PIL image RGB888->RGB565 conversion 96 | for i in range(0, len(pixelbytes), 4096): 97 | self.dhm.st7789.data(pixelbytes[i:i + 4096]) 98 | 99 | def run(self): 100 | self._running = True 101 | signal.signal(signal.SIGINT, self._exit) 102 | while self._running: 103 | for event in pygame.event.get(): 104 | if event.type == pygame.QUIT: 105 | self._running = False 106 | break 107 | if event.type == pygame.KEYDOWN: 108 | if event.key == pygame.K_ESCAPE: 109 | self._running = False 110 | break 111 | 112 | # Clear the screen 113 | self.screen.fill((0, 0, 0)) 114 | 115 | # Draw the demo effect 116 | t = int(time.time() * 40) 117 | for x in range(21): 118 | for y in range(15): 119 | r, g, b = tunnel(x, y, t) 120 | r = min(255, int(r)) 121 | g = min(255, int(g)) 122 | b = min(255, int(b)) 123 | pygame.draw.circle(self.screen, (r, g, b), ((x * 15) + 7, (y * 15) + 6 + 7), 7) 124 | 125 | self._updatefb() 126 | 127 | pygame.quit() 128 | sys.exit(0) 129 | 130 | 131 | display = PygameDHM() 132 | display.run() 133 | -------------------------------------------------------------------------------- /install-bullseye.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | CONFIG=/boot/config.txt 3 | DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` 4 | CONFIG_BACKUP=false 5 | APT_HAS_UPDATED=false 6 | USER_HOME=/home/$SUDO_USER 7 | RESOURCES_TOP_DIR=$USER_HOME/Pimoroni 8 | WD=`pwd` 9 | USAGE="sudo ./install.sh (--unstable)" 10 | POSITIONAL_ARGS=() 11 | UNSTABLE=false 12 | PYTHON="/usr/bin/python3" 13 | CODENAME=`lsb_release -sc` 14 | 15 | distro_check() { 16 | if [[ $CODENAME != "bullseye" ]]; then 17 | printf "This installer is for Raspberry Pi OS: Bullseye only, current distro: $CODENAME\n" 18 | exit 1 19 | fi 20 | } 21 | 22 | user_check() { 23 | if [ $(id -u) -ne 0 ]; then 24 | printf "Script must be run as root. Try 'sudo ./install.sh'\n" 25 | exit 1 26 | fi 27 | } 28 | 29 | confirm() { 30 | if [ "$FORCE" == '-y' ]; then 31 | true 32 | else 33 | read -r -p "$1 [y/N] " response < /dev/tty 34 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 35 | true 36 | else 37 | false 38 | fi 39 | fi 40 | } 41 | 42 | prompt() { 43 | read -r -p "$1 [y/N] " response < /dev/tty 44 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 45 | true 46 | else 47 | false 48 | fi 49 | } 50 | 51 | success() { 52 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 53 | } 54 | 55 | inform() { 56 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 57 | } 58 | 59 | warning() { 60 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 61 | } 62 | 63 | function do_config_backup { 64 | if [ ! $CONFIG_BACKUP == true ]; then 65 | CONFIG_BACKUP=true 66 | FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" 67 | inform "Backing up $CONFIG to /boot/$FILENAME\n" 68 | cp $CONFIG /boot/$FILENAME 69 | mkdir -p $RESOURCES_TOP_DIR/config-backups/ 70 | cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME 71 | if [ -f "$UNINSTALLER" ]; then 72 | echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER 73 | fi 74 | fi 75 | } 76 | 77 | function apt_pkg_install { 78 | PACKAGES=() 79 | PACKAGES_IN=("$@") 80 | for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do 81 | PACKAGE="${PACKAGES_IN[$i]}" 82 | if [ "$PACKAGE" == "" ]; then continue; fi 83 | printf "Checking for $PACKAGE\n" 84 | dpkg -L $PACKAGE > /dev/null 2>&1 85 | if [ "$?" == "1" ]; then 86 | PACKAGES+=("$PACKAGE") 87 | fi 88 | done 89 | PACKAGES="${PACKAGES[@]}" 90 | if ! [ "$PACKAGES" == "" ]; then 91 | echo "Installing missing packages: $PACKAGES" 92 | if [ ! $APT_HAS_UPDATED ]; then 93 | apt update 94 | APT_HAS_UPDATED=true 95 | fi 96 | apt install -y $PACKAGES 97 | if [ -f "$UNINSTALLER" ]; then 98 | echo "apt uninstall -y $PACKAGES" 99 | fi 100 | fi 101 | } 102 | 103 | while [[ $# -gt 0 ]]; do 104 | K="$1" 105 | case $K in 106 | -u|--unstable) 107 | UNSTABLE=true 108 | shift 109 | ;; 110 | -p|--python) 111 | PYTHON=$2 112 | shift 113 | shift 114 | ;; 115 | *) 116 | if [[ $1 == -* ]]; then 117 | printf "Unrecognised option: $1\n"; 118 | printf "Usage: $USAGE\n"; 119 | exit 1 120 | fi 121 | POSITIONAL_ARGS+=("$1") 122 | shift 123 | esac 124 | done 125 | 126 | distro_check 127 | user_check 128 | 129 | if [ ! -f "$PYTHON" ]; then 130 | printf "Python path $PYTHON not found!\n" 131 | exit 1 132 | fi 133 | 134 | PYTHON_VER=`$PYTHON --version` 135 | 136 | inform "Installing. Please wait..." 137 | 138 | $PYTHON -m pip install --upgrade configparser 139 | 140 | CONFIG_VARS=`$PYTHON - < $UNINSTALLER 177 | printf "It's recommended you run these steps manually.\n" 178 | printf "If you want to run the full script, open it in\n" 179 | printf "an editor and remove 'exit 1' from below.\n" 180 | exit 1 181 | EOF 182 | 183 | printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" 184 | 185 | if $UNSTABLE; then 186 | warning "Installing unstable library from source.\n\n" 187 | else 188 | printf "Installing stable library from pypi.\n\n" 189 | fi 190 | 191 | cd library 192 | 193 | printf "Installing for $PYTHON_VER...\n" 194 | apt_pkg_install "${PY3_DEPS[@]}" 195 | if $UNSTABLE; then 196 | $PYTHON setup.py install > /dev/null 197 | else 198 | $PYTHON -m pip install --upgrade $LIBRARY_NAME 199 | fi 200 | if [ $? -eq 0 ]; then 201 | success "Done!\n" 202 | echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER 203 | fi 204 | 205 | cd $WD 206 | 207 | for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do 208 | CMD="${SETUP_CMDS[$i]}" 209 | # Attempt to catch anything that touches /boot/config.txt and trigger a backup 210 | if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then 211 | do_config_backup 212 | fi 213 | eval $CMD 214 | done 215 | 216 | for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do 217 | CONFIG_LINE="${CONFIG_TXT[$i]}" 218 | if ! [ "$CONFIG_LINE" == "" ]; then 219 | do_config_backup 220 | inform "Adding $CONFIG_LINE to $CONFIG\n" 221 | sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG 222 | if ! grep -q "^$CONFIG_LINE" $CONFIG; then 223 | printf "$CONFIG_LINE\n" >> $CONFIG 224 | fi 225 | fi 226 | done 227 | 228 | if [ -d "examples" ]; then 229 | if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then 230 | inform "Copying examples to $RESOURCES_DIR" 231 | cp -r examples/ $RESOURCES_DIR 232 | echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER 233 | success "Done!" 234 | fi 235 | fi 236 | 237 | printf "\n" 238 | 239 | if [ -f "/usr/bin/pydoc" ]; then 240 | printf "Generating documentation.\n" 241 | pydoc -w $LIBRARY_NAME > /dev/null 242 | if [ -f "$LIBRARY_NAME.html" ]; then 243 | cp $LIBRARY_NAME.html $RESOURCES_DIR/docs.html 244 | rm -f $LIBRARY_NAME.html 245 | inform "Documentation saved to $RESOURCES_DIR/docs.html" 246 | success "Done!" 247 | else 248 | warning "Error: Failed to generate documentation." 249 | fi 250 | fi 251 | 252 | success "\nAll done!" 253 | inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" 254 | inform "Find uninstall steps in $UNINSTALLER\n" 255 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONFIG=/boot/config.txt 4 | DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` 5 | CONFIG_BACKUP=false 6 | APT_HAS_UPDATED=false 7 | USER_HOME=/home/$SUDO_USER 8 | RESOURCES_TOP_DIR=$USER_HOME/Pimoroni 9 | WD=`pwd` 10 | USAGE="sudo ./install.sh (--unstable)" 11 | POSITIONAL_ARGS=() 12 | UNSTABLE=false 13 | CODENAME=`lsb_release -sc` 14 | 15 | if [[ $CODENAME == "bullseye" ]]; then 16 | bash ./install-bullseye.sh $@ 17 | exit $? 18 | fi 19 | 20 | user_check() { 21 | if [ $(id -u) -ne 0 ]; then 22 | printf "Script must be run as root. Try 'sudo ./install.sh'\n" 23 | exit 1 24 | fi 25 | } 26 | 27 | confirm() { 28 | if [ "$FORCE" == '-y' ]; then 29 | true 30 | else 31 | read -r -p "$1 [y/N] " response < /dev/tty 32 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 33 | true 34 | else 35 | false 36 | fi 37 | fi 38 | } 39 | 40 | prompt() { 41 | read -r -p "$1 [y/N] " response < /dev/tty 42 | if [[ $response =~ ^(yes|y|Y)$ ]]; then 43 | true 44 | else 45 | false 46 | fi 47 | } 48 | 49 | success() { 50 | echo -e "$(tput setaf 2)$1$(tput sgr0)" 51 | } 52 | 53 | inform() { 54 | echo -e "$(tput setaf 6)$1$(tput sgr0)" 55 | } 56 | 57 | warning() { 58 | echo -e "$(tput setaf 1)$1$(tput sgr0)" 59 | } 60 | 61 | function do_config_backup { 62 | if [ ! $CONFIG_BACKUP == true ]; then 63 | CONFIG_BACKUP=true 64 | FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" 65 | inform "Backing up $CONFIG to /boot/$FILENAME\n" 66 | cp $CONFIG /boot/$FILENAME 67 | mkdir -p $RESOURCES_TOP_DIR/config-backups/ 68 | cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME 69 | if [ -f "$UNINSTALLER" ]; then 70 | echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER 71 | fi 72 | fi 73 | } 74 | 75 | function apt_pkg_install { 76 | PACKAGES=() 77 | PACKAGES_IN=("$@") 78 | for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do 79 | PACKAGE="${PACKAGES_IN[$i]}" 80 | if [ "$PACKAGE" == "" ]; then continue; fi 81 | printf "Checking for $PACKAGE\n" 82 | dpkg -L $PACKAGE > /dev/null 2>&1 83 | if [ "$?" == "1" ]; then 84 | PACKAGES+=("$PACKAGE") 85 | fi 86 | done 87 | PACKAGES="${PACKAGES[@]}" 88 | if ! [ "$PACKAGES" == "" ]; then 89 | echo "Installing missing packages: $PACKAGES" 90 | if [ ! $APT_HAS_UPDATED ]; then 91 | apt update 92 | APT_HAS_UPDATED=true 93 | fi 94 | apt install -y $PACKAGES 95 | if [ -f "$UNINSTALLER" ]; then 96 | echo "apt uninstall -y $PACKAGES" 97 | fi 98 | fi 99 | } 100 | 101 | while [[ $# -gt 0 ]]; do 102 | K="$1" 103 | case $K in 104 | -u|--unstable) 105 | UNSTABLE=true 106 | shift 107 | ;; 108 | *) 109 | if [[ $1 == -* ]]; then 110 | printf "Unrecognised option: $1\n"; 111 | printf "Usage: $USAGE\n"; 112 | exit 1 113 | fi 114 | POSITIONAL_ARGS+=("$1") 115 | shift 116 | esac 117 | done 118 | 119 | user_check 120 | 121 | apt_pkg_install python-configparser 122 | 123 | CONFIG_VARS=`python - < $UNINSTALLER 160 | printf "It's recommended you run these steps manually.\n" 161 | printf "If you want to run the full script, open it in\n" 162 | printf "an editor and remove 'exit 1' from below.\n" 163 | exit 1 164 | EOF 165 | 166 | printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Installer\n\n" 167 | 168 | if $UNSTABLE; then 169 | warning "Installing unstable library from source.\n\n" 170 | else 171 | printf "Installing stable library from pypi.\n\n" 172 | fi 173 | 174 | cd library 175 | 176 | printf "Installing for Python 2..\n" 177 | apt_pkg_install "${PY2_DEPS[@]}" 178 | if $UNSTABLE; then 179 | python setup.py install > /dev/null 180 | else 181 | pip install --upgrade $LIBRARY_NAME 182 | fi 183 | if [ $? -eq 0 ]; then 184 | success "Done!\n" 185 | echo "pip uninstall $LIBRARY_NAME" >> $UNINSTALLER 186 | fi 187 | 188 | if [ -f "/usr/bin/python3" ]; then 189 | printf "Installing for Python 3..\n" 190 | apt_pkg_install "${PY3_DEPS[@]}" 191 | if $UNSTABLE; then 192 | python3 setup.py install > /dev/null 193 | else 194 | pip3 install --upgrade $LIBRARY_NAME 195 | fi 196 | if [ $? -eq 0 ]; then 197 | success "Done!\n" 198 | echo "pip3 uninstall $LIBRARY_NAME" >> $UNINSTALLER 199 | fi 200 | fi 201 | 202 | cd $WD 203 | 204 | for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do 205 | CMD="${SETUP_CMDS[$i]}" 206 | # Attempt to catch anything that touches /boot/config.txt and trigger a backup 207 | if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then 208 | do_config_backup 209 | fi 210 | eval $CMD 211 | done 212 | 213 | for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do 214 | CONFIG_LINE="${CONFIG_TXT[$i]}" 215 | if ! [ "$CONFIG_LINE" == "" ]; then 216 | do_config_backup 217 | inform "Adding $CONFIG_LINE to $CONFIG\n" 218 | sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG 219 | if ! grep -q "^$CONFIG_LINE" $CONFIG; then 220 | printf "$CONFIG_LINE\n" >> $CONFIG 221 | fi 222 | fi 223 | done 224 | 225 | if [ -d "examples" ]; then 226 | if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then 227 | inform "Copying examples to $RESOURCES_DIR" 228 | cp -r examples/ $RESOURCES_DIR 229 | echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER 230 | success "Done!" 231 | fi 232 | fi 233 | 234 | printf "\n" 235 | 236 | if [ -f "/usr/bin/pydoc" ]; then 237 | printf "Generating documentation.\n" 238 | pydoc -w $LIBRARY_NAME > /dev/null 239 | if [ -f "$LIBRARY_NAME.html" ]; then 240 | cp $LIBRARY_NAME.html $RESOURCES_DIR/docs.html 241 | rm -f $LIBRARY_NAME.html 242 | inform "Documentation saved to $RESOURCES_DIR/docs.html" 243 | success "Done!" 244 | else 245 | warning "Error: Failed to generate documentation." 246 | fi 247 | fi 248 | 249 | success "\nAll done!" 250 | inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" 251 | inform "Find uninstall steps in $UNINSTALLER\n" 252 | -------------------------------------------------------------------------------- /library/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = displayhatmini 3 | omit = 4 | .tox/* 5 | -------------------------------------------------------------------------------- /library/CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 0.0.2 2 | ----- 3 | 4 | * Add PWM backlight support (not super stable, but it works) 5 | * Add `on_button_pressed` for interrupt driven buttons 6 | 7 | 0.0.1 8 | ----- 9 | 10 | * Initial Release 11 | -------------------------------------------------------------------------------- /library/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pimoroni Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /library/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.txt 2 | include LICENSE.txt 3 | include README.md 4 | include setup.py 5 | recursive-include displayhatmini *.py 6 | -------------------------------------------------------------------------------- /library/README.md: -------------------------------------------------------------------------------- 1 | # Display HAT Mini 2 | 3 | [![Build Status](https://shields.io/github/workflow/status/pimoroni/displayhatmini-python/Python%20Tests.svg)](https://github.com/pimoroni/displayhatmini-python/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/displayhatmini-python/badge.svg?branch=main)](https://coveralls.io/github/pimoroni/displayhatmini-python?branch=main) 5 | [![PyPi Package](https://img.shields.io/pypi/v/displayhatmini.svg)](https://pypi.python.org/pypi/displayhatmini) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/displayhatmini.svg)](https://pypi.python.org/pypi/displayhatmini) 7 | 8 | # Pre-requisites 9 | 10 | You must enable: 11 | 12 | * spi: `sudo raspi-config nonint do_spi 0` 13 | 14 | You can optionally run `sudo raspi-config` or the graphical Raspberry Pi Configuration UI to enable interfaces. 15 | 16 | # Installing 17 | 18 | Stable library from PyPi: 19 | 20 | * Just run `pip3 install displayhatmini` 21 | 22 | In some cases you may need to use `sudo` or install pip with: `sudo apt install python3-pip` 23 | 24 | Latest/development library from GitHub: 25 | 26 | * `git clone https://github.com/pimoroni/displayhatmini-python` 27 | * `cd displayhatmini-python` 28 | * `sudo ./install.sh` 29 | 30 | # Useful Links 31 | 32 | * Pinout - https://pinout.xyz/pinout/display_hat_mini 33 | * Buy - https://shop.pimoroni.com/products/display-hat-mini 34 | # Changelog 35 | 0.0.2 36 | ----- 37 | 38 | * Add PWM backlight support (not super stable, but it works) 39 | * Add `on_button_pressed` for interrupt driven buttons 40 | 41 | 0.0.1 42 | ----- 43 | 44 | * Initial Release 45 | -------------------------------------------------------------------------------- /library/displayhatmini/__init__.py: -------------------------------------------------------------------------------- 1 | import RPi.GPIO as GPIO 2 | from ST7789 import ST7789 3 | 4 | 5 | __version__ = '0.0.2' 6 | 7 | 8 | class DisplayHATMini(): 9 | # User buttons 10 | BUTTON_A = 5 11 | BUTTON_B = 6 12 | BUTTON_X = 16 13 | BUTTON_Y = 24 14 | 15 | # Onboard RGB LED 16 | LED_R = 17 17 | LED_G = 27 18 | LED_B = 22 19 | 20 | # LCD Pins 21 | SPI_PORT = 0 22 | SPI_CS = 1 23 | SPI_DC = 9 24 | BACKLIGHT = 13 25 | 26 | # LCD Size 27 | WIDTH = 320 28 | HEIGHT = 240 29 | 30 | def __init__(self, buffer, backlight_pwm=False): 31 | """Initialise displayhatmini 32 | """ 33 | 34 | self.buffer = buffer 35 | GPIO.setwarnings(False) 36 | GPIO.setmode(GPIO.BCM) 37 | 38 | # Setup user buttons 39 | GPIO.setup(self.BUTTON_A, GPIO.IN, pull_up_down=GPIO.PUD_UP) 40 | GPIO.setup(self.BUTTON_B, GPIO.IN, pull_up_down=GPIO.PUD_UP) 41 | GPIO.setup(self.BUTTON_X, GPIO.IN, pull_up_down=GPIO.PUD_UP) 42 | GPIO.setup(self.BUTTON_Y, GPIO.IN, pull_up_down=GPIO.PUD_UP) 43 | 44 | # Setup user LEDs 45 | GPIO.setup(self.LED_R, GPIO.OUT) 46 | GPIO.setup(self.LED_G, GPIO.OUT) 47 | GPIO.setup(self.LED_B, GPIO.OUT) 48 | 49 | self.led_r_pwm = GPIO.PWM(self.LED_R, 2000) 50 | self.led_r_pwm.start(100) 51 | 52 | self.led_g_pwm = GPIO.PWM(self.LED_G, 2000) 53 | self.led_g_pwm.start(100) 54 | 55 | self.led_b_pwm = GPIO.PWM(self.LED_B, 2000) 56 | self.led_b_pwm.start(100) 57 | 58 | if backlight_pwm: 59 | GPIO.setup(self.BACKLIGHT, GPIO.OUT) 60 | self.backlight_pwm = GPIO.PWM(self.BACKLIGHT, 500) 61 | self.backlight_pwm.start(100) 62 | else: 63 | self.backlight_pwm = None 64 | 65 | self.st7789 = ST7789( 66 | port=self.SPI_PORT, 67 | cs=self.SPI_CS, 68 | dc=self.SPI_DC, 69 | backlight=None if backlight_pwm else self.BACKLIGHT, 70 | width=self.WIDTH, 71 | height=self.HEIGHT, 72 | rotation=180, 73 | spi_speed_hz=60 * 1000 * 1000 74 | ) 75 | 76 | def __del__(self): 77 | GPIO.cleanup() 78 | 79 | def set_led(self, r=0, g=0, b=0): 80 | if r < 0.0 or r > 1.0: 81 | raise ValueError("r must be in the range 0.0 to 1.0") 82 | elif g < 0.0 or g > 1.0: 83 | raise ValueError("g must be in the range 0.0 to 1.0") 84 | elif b < 0.0 or b > 1.0: 85 | raise ValueError("b must be in the range 0.0 to 1.0") 86 | else: 87 | self.led_r_pwm.ChangeDutyCycle((1.0 - r) * 100) 88 | self.led_g_pwm.ChangeDutyCycle((1.0 - g) * 100) 89 | self.led_b_pwm.ChangeDutyCycle((1.0 - b) * 100) 90 | 91 | def set_backlight(self, value): 92 | if self.backlight_pwm is not None: 93 | self.backlight_pwm.ChangeDutyCycle(value * 100) 94 | else: 95 | self.st7789.set_backlight(int(value)) 96 | 97 | def on_button_pressed(self, callback): 98 | for pin in (self.BUTTON_A, self.BUTTON_B, self.BUTTON_X, self.BUTTON_Y): 99 | GPIO.add_event_detect(pin, GPIO.BOTH, callback=callback) 100 | 101 | def read_button(self, pin): 102 | return not GPIO.input(pin) 103 | 104 | def display(self): 105 | self.st7789.display(self.buffer) 106 | -------------------------------------------------------------------------------- /library/displayhatmini/__main__.py: -------------------------------------------------------------------------------- 1 | import time 2 | from PIL import Image, ImageDraw 3 | from . import DisplayHATMini 4 | 5 | 6 | print("""DisplayHATMini Function Test 7 | 8 | Press Ctrl + C to exit! 9 | 10 | """) 11 | 12 | width = DisplayHATMini.WIDTH 13 | height = DisplayHATMini.HEIGHT 14 | buffer = Image.new("RGB", (width, height)) 15 | draw = ImageDraw.Draw(buffer) 16 | 17 | displayhatmini = DisplayHATMini(buffer) 18 | displayhatmini.set_led(0.05, 0.05, 0.05) 19 | 20 | while True: 21 | draw.rectangle((0, 0, width, height), (0, 0, 0)) 22 | 23 | if displayhatmini.read_button(displayhatmini.BUTTON_A): 24 | displayhatmini.set_led(1.0, 0.0, 0.0) 25 | draw.rectangle((0, 0, 50, 50), (255, 192, 192)) 26 | else: 27 | draw.rectangle((0, 0, 50, 50), (255, 0, 0)) 28 | 29 | if displayhatmini.read_button(displayhatmini.BUTTON_B): 30 | displayhatmini.set_led(0.0, 0.0, 1.0) 31 | draw.rectangle((0, height - 50, 50, height), (192, 192, 255)) 32 | else: 33 | draw.rectangle((0, height - 50, 50, height), (0, 0, 255)) 34 | 35 | if displayhatmini.read_button(displayhatmini.BUTTON_X): 36 | displayhatmini.set_led(0.0, 1.0, 0.0) 37 | draw.rectangle((width - 50, 0, width, 50), (192, 255, 192)) 38 | else: 39 | draw.rectangle((width - 50, 0, width, 50), (0, 255, 0)) 40 | 41 | if displayhatmini.read_button(displayhatmini.BUTTON_Y): 42 | displayhatmini.set_led(1.0, 1.0, 0.0) 43 | draw.rectangle((width - 50, height - 50, width, height), (255, 255, 192)) 44 | else: 45 | draw.rectangle((width - 50, height - 50, width, height), (255, 255, 0)) 46 | 47 | displayhatmini.display() 48 | time.sleep(0.01) 49 | -------------------------------------------------------------------------------- /library/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=40.8.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /library/setup.cfg: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | [metadata] 3 | name = displayhatmini 4 | version = 0.0.2 5 | author = Philip Howard 6 | author_email = phil@pimoroni.com 7 | description = Python library for the displayhatmini 320x240 LCD with buttons and RGB 8 | long_description = file: README.md 9 | long_description_content_type = text/markdown 10 | keywords = Raspberry Pi 11 | url = https://www.pimoroni.com 12 | project_urls = 13 | GitHub=https://www.github.com/pimoroni/displayhatmini-python 14 | license = MIT 15 | # This includes the license file(s) in the wheel. 16 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file 17 | license_files = LICENSE.txt 18 | classifiers = 19 | Development Status :: 4 - Beta 20 | Operating System :: POSIX :: Linux 21 | License :: OSI Approved :: MIT License 22 | Intended Audience :: Developers 23 | Programming Language :: Python :: 2.7 24 | Programming Language :: Python :: 3 25 | Topic :: Software Development 26 | Topic :: Software Development :: Libraries 27 | Topic :: System :: Hardware 28 | 29 | [options] 30 | python_requires = >= 2.7 31 | packages = displayhatmini 32 | install_requires = 33 | st7789 >= 0.0.4 34 | RPi.GPIO 35 | 36 | [flake8] 37 | exclude = 38 | .tox, 39 | .eggs, 40 | .git, 41 | __pycache__, 42 | build, 43 | dist 44 | ignore = 45 | E501 46 | 47 | [pimoroni] 48 | py2deps = 49 | py3deps = 50 | configtxt = 51 | commands = 52 | -------------------------------------------------------------------------------- /library/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Copyright (c) 2016 Pimoroni 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | 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 | """ 24 | 25 | from setuptools import setup, __version__ 26 | from pkg_resources import parse_version 27 | 28 | minimum_version = parse_version('30.4.0') 29 | 30 | if parse_version(__version__) < minimum_version: 31 | raise RuntimeError("Package setuptools must be at least version {}".format(minimum_version)) 32 | 33 | setup() 34 | -------------------------------------------------------------------------------- /library/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import mock 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope='function', autouse=False) 7 | def GPIO(): 8 | """Mock RPi.GPIO module.""" 9 | GPIO = mock.MagicMock() 10 | # Fudge for Python < 37 (possibly earlier) 11 | sys.modules['RPi'] = mock.MagicMock() 12 | sys.modules['RPi'].GPIO = GPIO 13 | sys.modules['RPi.GPIO'] = GPIO 14 | yield GPIO 15 | del sys.modules['RPi'] 16 | del sys.modules['RPi.GPIO'] 17 | 18 | 19 | @pytest.fixture(scope='function', autouse=False) 20 | def ST7789(PIL): 21 | """Mock ST7789 module.""" 22 | ST7789 = mock.MagicMock() 23 | sys.modules['ST7789'] = ST7789 24 | yield ST7789 25 | del sys.modules['ST7789'] 26 | 27 | 28 | @pytest.fixture(scope='function', autouse=False) 29 | def displayhatmini(): 30 | """Import Display HAT mini.""" 31 | import displayhatmini 32 | yield displayhatmini 33 | del sys.modules['displayhatmini'] 34 | 35 | 36 | @pytest.fixture(scope='function', autouse=False) 37 | def PIL(): 38 | """Mock PIL module.""" 39 | PIL = mock.MagicMock() 40 | sys.modules['PIL'] = PIL 41 | yield PIL 42 | del sys.modules['PIL'] 43 | -------------------------------------------------------------------------------- /library/tests/test_setup.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | 4 | def test_setup(GPIO, ST7789, displayhatmini): 5 | display = displayhatmini.DisplayHATMini(bytearray()) 6 | 7 | GPIO.setup.assert_has_calls(( 8 | mock.call(display.BUTTON_A, GPIO.IN, pull_up_down=GPIO.PUD_UP), 9 | mock.call(display.BUTTON_B, GPIO.IN, pull_up_down=GPIO.PUD_UP), 10 | mock.call(display.BUTTON_X, GPIO.IN, pull_up_down=GPIO.PUD_UP), 11 | mock.call(display.BUTTON_Y, GPIO.IN, pull_up_down=GPIO.PUD_UP), 12 | 13 | mock.call(display.LED_R, GPIO.OUT), 14 | mock.call(display.LED_G, GPIO.OUT), 15 | mock.call(display.LED_B, GPIO.OUT) 16 | )) 17 | -------------------------------------------------------------------------------- /library/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,35,37,39},qa 3 | skip_missing_interpreters = True 4 | 5 | [testenv] 6 | commands = 7 | python setup.py install 8 | coverage run -m py.test -v -r wsx 9 | coverage report 10 | deps = 11 | mock 12 | pytest>=3.1 13 | pytest-cov 14 | 15 | [testenv:qa] 16 | commands = 17 | check-manifest --ignore tox.ini,tests/*,.coveragerc 18 | python setup.py sdist bdist_wheel 19 | twine check dist/* 20 | flake8 --ignore E501 21 | deps = 22 | check-manifest 23 | flake8 24 | twine 25 | 26 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LIBRARY_VERSION=`cat library/setup.cfg | grep version | awk -F" = " '{print $2}'` 4 | LIBRARY_NAME=`cat library/setup.cfg | grep name | awk -F" = " '{print $2}'` 5 | 6 | printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Uninstaller\n\n" 7 | 8 | if [ $(id -u) -ne 0 ]; then 9 | printf "Script must be run as root. Try 'sudo ./uninstall.sh'\n" 10 | exit 1 11 | fi 12 | 13 | cd library 14 | 15 | printf "Unnstalling for Python 2..\n" 16 | pip uninstall $LIBRARY_NAME 17 | 18 | if [ -f "/usr/bin/pip3" ]; then 19 | printf "Uninstalling for Python 3..\n" 20 | pip3 uninstall $LIBRARY_NAME 21 | fi 22 | 23 | cd .. 24 | 25 | printf "Done!\n" 26 | --------------------------------------------------------------------------------