├── .github └── workflows │ └── test.yml ├── .gitignore ├── .stickler.yml ├── LICENSE ├── Makefile ├── README.md ├── examples ├── clock.py ├── demo.py ├── hue.py ├── test.py └── uinput-touch.py ├── install-bullseye.sh ├── install.sh ├── library ├── .coveragerc ├── CHANGELOG.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── hyperpixel2r │ ├── __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 | - master 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; python 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 | # HyperPixel 2" Round Touch Driver 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/pimoroni/hyperpixel2r-python/Python%20Tests)](https://github.com/pimoroni/hyperpixel2r-python/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/hyperpixel2r-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/hyperpixel2r-python?branch=master) 5 | [![PyPi Package](https://img.shields.io/pypi/v/hyperpixel2r.svg)](https://pypi.python.org/pypi/hyperpixel2r) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/hyperpixel2r.svg)](https://pypi.python.org/pypi/hyperpixel2r) 7 | 8 | # Pre-requisites 9 | 10 | You must install the HyperPixel 2r drivers which enable an i2c bus for the touch IC - https://github.com/pimoroni/hyperpixel2r 11 | 12 | Make sure you edit `/boot/config.txt` and add `:disable-touch` after `hyperpixel2r`, like so: 13 | 14 | ``` 15 | dtoverlay=hyperpixel2r:disable-touch 16 | ``` 17 | 18 | This disables the Linux touch driver so Python can talk to the touch IC. 19 | 20 | # Installing 21 | 22 | Stable library from PyPi: 23 | 24 | * Just run `pip3 install hyperpixel2r` 25 | 26 | In some cases you may need to use `sudo` or install pip with: `sudo apt install python3-pip` 27 | 28 | Latest/development library from GitHub: 29 | 30 | * `git clone https://github.com/pimoroni/hyperpixel2r-python` 31 | * `cd hyperpixel2r-python` 32 | * `sudo ./install.sh` 33 | 34 | # SDL/Pygame on Raspberry Pi 35 | 36 | ## pygame.error: No video mode large enough for 640x480 37 | 38 | The version of Pygame shipped with Raspberry Pi OS doesn't like non-standard resolutions like 480x480. You can fake a 640x480 standard display by forcing HDMI hotplug, and then just to a 480x480 region to display on HyperPixel 2.0" round. In `/boot/config.txt`: 39 | 40 | ```text 41 | # Force 640x480 video for Pygame / HyperPixel2r 42 | hdmi_force_hotplug=1 43 | hdmi_mode=1 44 | hdmi_group=1 45 | ``` 46 | 47 | # Usage 48 | 49 | Set up touch driver instance: 50 | 51 | ```python 52 | touch = Touch(bus=11, i2c_addr=0x15, interrupt_pin=27): 53 | ``` 54 | 55 | Touches should be read by decorating a handler with `@touch.on_touch`. 56 | 57 | The handler should accept the arguments `touch_id`, `x`, `y` and `state`. 58 | 59 | * `touch_id` - 0 or 1 depending on which touch is tracked 60 | * `x` - x coordinate from 0 to 479 61 | * `y` - y coordinate from 0 to 479 62 | * `state` - touch state `True` for touched, `False` for released 63 | 64 | For example: 65 | 66 | ```python 67 | @touch.on_touch 68 | def handle_touch(touch_id, x, y, state): 69 | print(touch_id, x, y, state) 70 | ``` 71 | -------------------------------------------------------------------------------- /examples/clock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import signal 5 | import pygame 6 | from pygame import gfxdraw 7 | import math 8 | import time 9 | import datetime 10 | from colorsys import hsv_to_rgb 11 | from hyperpixel2r import Touch 12 | 13 | 14 | """ 15 | HyperPixel 2 Clock 16 | 17 | Run with: sudo SDL_FBDEV=/dev/fb0 python3 clock.py 18 | """ 19 | 20 | 21 | class Hyperpixel2r: 22 | screen = None 23 | def __init__(self): 24 | self._init_display() 25 | 26 | self.screen.fill((0, 0, 0)) 27 | if self._rawfb: 28 | self._updatefb() 29 | else: 30 | pygame.display.update() 31 | 32 | # For some reason the canvas needs a 7px vertical offset 33 | # circular screens are weird... 34 | self.center = (240, 247) 35 | self._radius = 240 36 | 37 | # Distance of hour marks from center 38 | self._marks = 220 39 | 40 | self._running = False 41 | self._origin = pygame.math.Vector2(*self.center) 42 | self._clock = pygame.time.Clock() 43 | self._colour = (255, 0, 255) 44 | 45 | def _exit(self, sig, frame): 46 | self._running = False 47 | print("\nExiting!...\n") 48 | 49 | def _init_display(self): 50 | self._rawfb = False 51 | # Based on "Python GUI in Linux frame buffer" 52 | # http://www.karoltomala.com/blog/?p=679 53 | DISPLAY = os.getenv("DISPLAY") 54 | if DISPLAY: 55 | print("Display: {0}".format(DISPLAY)) 56 | 57 | if os.getenv('SDL_VIDEODRIVER'): 58 | print("Using driver specified by SDL_VIDEODRIVER: {}".format(os.getenv('SDL_VIDEODRIVER'))) 59 | pygame.display.init() 60 | size = (pygame.display.Info().current_w, pygame.display.Info().current_h) 61 | if size == (480, 480): # Fix for 480x480 mode offset 62 | size = (640, 480) 63 | self.screen = pygame.display.set_mode(size, pygame.FULLSCREEN | pygame.DOUBLEBUF | pygame.NOFRAME | pygame.HWSURFACE) 64 | return 65 | 66 | else: 67 | # Iterate through drivers and attempt to init/set_mode 68 | for driver in ['rpi', 'kmsdrm', 'fbcon', 'directfb', 'svgalib']: 69 | os.putenv('SDL_VIDEODRIVER', driver) 70 | try: 71 | pygame.display.init() 72 | size = (pygame.display.Info().current_w, pygame.display.Info().current_h) 73 | if size == (480, 480): # Fix for 480x480 mode offset 74 | size = (640, 480) 75 | self.screen = pygame.display.set_mode(size, pygame.FULLSCREEN | pygame.DOUBLEBUF | pygame.NOFRAME | pygame.HWSURFACE) 76 | print("Using driver: {0}, Framebuffer size: {1:d} x {2:d}".format(driver, *size)) 77 | return 78 | except pygame.error as e: 79 | print('Driver "{0}" failed: {1}'.format(driver, e)) 80 | continue 81 | break 82 | 83 | print("All SDL drivers failed, falling back to raw framebuffer access.") 84 | self._rawfb = True 85 | os.putenv('SDL_VIDEODRIVER', 'dummy') 86 | pygame.display.init() # Need to init for .convert() to work 87 | self.screen = pygame.Surface((480, 480)) 88 | 89 | def __del__(self): 90 | "Destructor to make sure pygame shuts down, etc." 91 | 92 | def touch(self, x, y, state): 93 | touch = pygame.math.Vector2(x, y) 94 | distance = self._origin.distance_to(touch) 95 | angle = pygame.math.Vector2().angle_to(self._origin - touch) 96 | angle %= 360 97 | 98 | value = (distance / 240.0) 99 | value = min(1.0, value) 100 | self._colour = tuple([int(c * 255) for c in hsv_to_rgb(angle / 360.0, value, 1.0)]) 101 | 102 | def _get_point(self, origin, angle, distance): 103 | r = math.radians(angle) 104 | cos = math.cos(r) 105 | sin = math.sin(r) 106 | x = origin[0] - distance * cos 107 | y = origin[1] - distance * sin 108 | return x, y 109 | 110 | def _circle(self, colour, center, radius, antialias=True): 111 | x, y = center 112 | if antialias: 113 | gfxdraw.aacircle(self.screen, x, y, radius, colour) 114 | gfxdraw.filled_circle(self.screen, x, y, radius, colour) 115 | 116 | def _line(self, colour, start, end, thickness): 117 | # Draw a filled, antialiased line with a given thickness 118 | # there's no pygame builtin for this so we get technical. 119 | start = pygame.math.Vector2(start) 120 | end = pygame.math.Vector2(end) 121 | 122 | # get the angle between the start/end points 123 | angle = pygame.math.Vector2().angle_to(end - start) 124 | 125 | # angle_to returns degrees, sin/cos need radians 126 | angle = math.radians(angle) 127 | 128 | sin = math.sin(angle) 129 | cos = math.cos(angle) 130 | 131 | # Find the center of the line 132 | center = (start + end) / 2.0 133 | 134 | # Get the length of the line, 135 | # half it, because we're drawing out from the center 136 | length = (start - end).length() / 2.0 137 | 138 | # half thickness, for the same reason 139 | thickness /= 2.0 140 | 141 | tl = (center.x + length * cos - thickness * sin, 142 | center.y + thickness * cos + length * sin) 143 | tr = (center.x - length * cos - thickness * sin, 144 | center.y + thickness * cos - length * sin) 145 | bl = (center.x + length * cos + thickness * sin, 146 | center.y - thickness * cos + length * sin) 147 | br = (center.x - length * cos + thickness * sin, 148 | center.y - thickness * cos - length * sin) 149 | 150 | gfxdraw.aapolygon(self.screen, (tl, tr, br, bl), colour) 151 | gfxdraw.filled_polygon(self.screen, (tl, tr, br, bl), colour) 152 | 153 | def _updatefb(self): 154 | fbdev = os.getenv('SDL_FBDEV', '/dev/fb0') 155 | with open(fbdev, 'wb') as fb: 156 | fb.write(self.screen.convert(16, 0).get_buffer()) 157 | 158 | def run(self): 159 | self._running = True 160 | signal.signal(signal.SIGINT, self._exit) 161 | while self._running: 162 | for event in pygame.event.get(): 163 | if event.type == pygame.QUIT: 164 | self._running = False 165 | break 166 | if event.type == pygame.KEYDOWN: 167 | if event.key == pygame.K_ESCAPE: 168 | self._running = False 169 | break 170 | 171 | # self._colour = tuple([int(c * 255) for c in hsv_to_rgb(time.time() / 12.0, 1.0, 1.0)]) 172 | now = datetime.datetime.now() 173 | 174 | a_s = now.second / 60.0 * 360.0 175 | 176 | a_m = now.minute / 60.0 * 360.0 177 | a_m += (now.second / 60.0) * (360.0 / 60) 178 | 179 | a_h = (now.hour % 12) / 12.0 * 360.0 180 | a_h += (now.minute / 60.0) * (360.0 / 12) 181 | 182 | a_s += 90 183 | a_m += 90 184 | a_h += 90 185 | 186 | a_s %= 360 187 | a_m %= 360 188 | a_h %= 360 189 | 190 | point_second_start = self._get_point(self.center, a_s, 10) 191 | point_second_end = self._get_point(self.center, a_s, self._marks - 30) 192 | 193 | point_minute_start = self._get_point(self.center, a_m, 10) 194 | point_minute_end = self._get_point(self.center, a_m, self._marks - 60) 195 | 196 | point_hour_start = self._get_point(self.center, a_h, 10) 197 | point_hour_end = self._get_point(self.center, a_h, self._marks - 90) 198 | 199 | # Clear the center of the clock 200 | # Black circle on a black background so we don't care about aa 201 | self._circle((0, 0, 0), self.center, self._radius, antialias=False) 202 | 203 | for s in range(60): 204 | a = 360 / 60.0 * s 205 | end = self._get_point(self.center, a, self._marks + 5) 206 | self._line(self._colour, self.center, end, 3) 207 | 208 | self._circle((0, 0, 0), self.center, self._marks - 5) 209 | 210 | for s in range(12): 211 | a = 360 / 12.0 * s 212 | x, y = self._get_point(self.center, a, self._marks) 213 | 214 | r = 5 215 | if s % 3 == 0: 216 | r = 10 217 | 218 | x = int(x) 219 | y = int(y) 220 | 221 | self._circle(self._colour, (x, y), r) 222 | 223 | # Draw the second, minute and hour hands 224 | self._line(self._colour, point_second_start, point_second_end, 3) 225 | self._line(self._colour, point_minute_start, point_minute_end, 6) 226 | self._line(self._colour, point_hour_start, point_hour_end, 11) 227 | 228 | # Draw the hub 229 | self._circle((0, 0, 0), self.center, 20) 230 | self._circle(self._colour, self.center, 10) 231 | 232 | if self._rawfb: 233 | self._updatefb() 234 | else: 235 | pygame.display.flip() 236 | self._clock.tick(30) # Aim for 30fps 237 | 238 | pygame.quit() 239 | sys.exit(0) 240 | 241 | 242 | display = Hyperpixel2r() 243 | touch = Touch() 244 | 245 | 246 | @touch.on_touch 247 | def handle_touch(touch_id, x, y, state): 248 | display.touch(x, y, state) 249 | 250 | 251 | display.run() 252 | -------------------------------------------------------------------------------- /examples/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 | from hyperpixel2r import Touch 10 | 11 | 12 | print("""HyperPixel 2 Lots of Circles Demo 13 | 14 | Run with: sudo SDL_FBDEV=/dev/fb0 python3 demo.py 15 | 16 | """) 17 | 18 | 19 | hue_to_rgb = [] 20 | 21 | 22 | for i in range(0, 255): 23 | hue_to_rgb.append(colorsys.hsv_to_rgb(i / 255.0, 1, 1)) 24 | 25 | 26 | # zoom tunnel 27 | def tunnel(x, y, step): 28 | u_width = 32 29 | u_height = 32 30 | speed = step / 100.0 31 | x -= (u_width / 2) 32 | y -= (u_height / 2) 33 | xo = math.sin(step / 27.0) * 2 34 | yo = math.cos(step / 18.0) * 2 35 | x += xo 36 | y += yo 37 | if y == 0: 38 | if x < 0: 39 | angle = -(math.pi / 2) 40 | else: 41 | angle = (math.pi / 2) 42 | else: 43 | angle = math.atan(x / y) 44 | if y > 0: 45 | angle += math.pi 46 | angle /= 2 * math.pi # convert angle to 0...1 range 47 | hyp = math.sqrt(math.pow(x, 2) + math.pow(y, 2)) 48 | shade = hyp / 2.1 49 | shade = 1 if shade > 1 else shade 50 | angle += speed 51 | depth = speed + (hyp / 10) 52 | col1 = hue_to_rgb[step % 255] 53 | col1 = (col1[0] * 0.8, col1[1] * 0.8, col1[2] * 0.8) 54 | col2 = hue_to_rgb[step % 255] 55 | col2 = (col2[0] * 0.3, col2[1] * 0.3, col2[2] * 0.3) 56 | col = col1 if int(abs(angle * 6.0)) % 2 == 0 else col2 57 | td = .3 if int(abs(depth * 3.0)) % 2 == 0 else 0 58 | col = (col[0] + td, col[1] + td, col[2] + td) 59 | col = (col[0] * shade, col[1] * shade, col[2] * shade) 60 | return (col[0] * 255, col[1] * 255, col[2] * 255) 61 | 62 | 63 | class Hyperpixel2r: 64 | screen = None 65 | 66 | def __init__(self): 67 | self._init_display() 68 | 69 | self.screen.fill((0, 0, 0)) 70 | if self._rawfb: 71 | self._updatefb() 72 | else: 73 | pygame.display.update() 74 | 75 | self._running = False 76 | 77 | def _exit(self, sig, frame): 78 | self._running = False 79 | print("\nExiting!...\n") 80 | 81 | def _init_display(self): 82 | self._rawfb = False 83 | # Based on "Python GUI in Linux frame buffer" 84 | # http://www.karoltomala.com/blog/?p=679 85 | DISPLAY = os.getenv("DISPLAY") 86 | if DISPLAY: 87 | print("Display: {0}".format(DISPLAY)) 88 | 89 | if os.getenv('SDL_VIDEODRIVER'): 90 | print("Using driver specified by SDL_VIDEODRIVER: {}".format(os.getenv('SDL_VIDEODRIVER'))) 91 | pygame.display.init() 92 | size = (pygame.display.Info().current_w, pygame.display.Info().current_h) 93 | if size == (480, 480): # Fix for 480x480 mode offset 94 | size = (640, 480) 95 | self.screen = pygame.display.set_mode(size, pygame.FULLSCREEN | pygame.DOUBLEBUF | pygame.NOFRAME | pygame.HWSURFACE) 96 | return 97 | 98 | else: 99 | # Iterate through drivers and attempt to init/set_mode 100 | for driver in ['rpi', 'kmsdrm', 'fbcon', 'directfb', 'svgalib']: 101 | os.putenv('SDL_VIDEODRIVER', driver) 102 | try: 103 | pygame.display.init() 104 | size = (pygame.display.Info().current_w, pygame.display.Info().current_h) 105 | if size == (480, 480): # Fix for 480x480 mode offset 106 | size = (640, 480) 107 | self.screen = pygame.display.set_mode(size, pygame.FULLSCREEN | pygame.DOUBLEBUF | pygame.NOFRAME | pygame.HWSURFACE) 108 | print("Using driver: {0}, Framebuffer size: {1:d} x {2:d}".format(driver, *size)) 109 | return 110 | except pygame.error as e: 111 | print('Driver "{0}" failed: {1}'.format(driver, e)) 112 | continue 113 | break 114 | 115 | print("All SDL drivers failed, falling back to raw framebuffer access.") 116 | self._rawfb = True 117 | os.putenv('SDL_VIDEODRIVER', 'dummy') 118 | pygame.display.init() # Need to init for .convert() to work 119 | self.screen = pygame.Surface((480, 480)) 120 | 121 | def __del__(self): 122 | "Destructor to make sure pygame shuts down, etc." 123 | 124 | def _updatefb(self): 125 | fbdev = os.getenv('SDL_FBDEV', '/dev/fb0') 126 | with open(fbdev, 'wb') as fb: 127 | fb.write(self.screen.convert(16, 0).get_buffer()) 128 | 129 | def run(self): 130 | self._running = True 131 | signal.signal(signal.SIGINT, self._exit) 132 | while self._running: 133 | for event in pygame.event.get(): 134 | if event.type == pygame.QUIT: 135 | self._running = False 136 | break 137 | if event.type == pygame.KEYDOWN: 138 | if event.key == pygame.K_ESCAPE: 139 | self._running = False 140 | break 141 | 142 | t = int(time.time() * 40) 143 | for x in range(32): 144 | for y in range(32): 145 | r, g, b = tunnel(x, y, t) 146 | r = min(255, int(r)) 147 | g = min(255, int(g)) 148 | b = min(255, int(b)) 149 | pygame.draw.circle(self.screen, (r, g, b), ((x * 15) + 6, (y * 15) + 6 + 7), 7) 150 | 151 | if self._rawfb: 152 | self._updatefb() 153 | else: 154 | pygame.display.flip() 155 | pygame.quit() 156 | sys.exit(0) 157 | 158 | def touch(self, x, y, state): 159 | pass 160 | 161 | 162 | display = Hyperpixel2r() 163 | touch = Touch() 164 | 165 | 166 | @touch.on_touch 167 | def handle_touch(touch_id, x, y, state): 168 | display.touch(x, y, state) 169 | 170 | 171 | display.run() 172 | -------------------------------------------------------------------------------- /examples/hue.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | import signal 5 | import pygame 6 | import math 7 | from colorsys import hsv_to_rgb 8 | from hyperpixel2r import Touch 9 | # import rgbmatrix5x5 10 | 11 | 12 | """ 13 | HyperPixel 2 Hue 14 | 15 | Run with: sudo SDL_FBDEV=/dev/fb0 python3 hue.py 16 | """ 17 | 18 | 19 | class Hyperpixel2r: 20 | screen = None 21 | 22 | def __init__(self): 23 | self._init_display() 24 | 25 | self.screen.fill((0, 0, 0)) 26 | 27 | if self._rawfb: 28 | self._updatefb() 29 | else: 30 | pygame.display.update() 31 | 32 | # For some reason the canvas needs a 7px vertical offset 33 | # circular screens are weird... 34 | self.center = (240, 247) 35 | self.radius = 240 36 | self.inner_radius = 150 37 | 38 | self._running = False 39 | self._hue = 0 40 | self._val = 1.0 41 | self._origin = pygame.math.Vector2(*self.center) 42 | self._clock = pygame.time.Clock() 43 | 44 | # Draw the hue wheel as lines emenating from the inner to outer radius 45 | # we overdraw 3x as many lines to get a nice solid fill... horribly inefficient but it works 46 | for s in range(360 * 3): 47 | a = s / 3.0 48 | cos = math.cos(math.radians(a)) 49 | sin = math.sin(math.radians(a)) 50 | x = self.center[0] - self.radius * cos 51 | y = self.center[1] - self.radius * sin 52 | 53 | ox = self.center[0] - self.inner_radius * cos 54 | oy = self.center[1] - self.inner_radius * sin 55 | 56 | colour = tuple([int(c * 255) for c in hsv_to_rgb(a / 360.0, 1.0, 1.0)]) 57 | pygame.draw.line(self.screen, colour, (ox, oy), (x, y), 3) 58 | 59 | def _exit(self, sig, frame): 60 | self._running = False 61 | print("\nExiting!...\n") 62 | 63 | def _init_display(self): 64 | self._rawfb = False 65 | # Based on "Python GUI in Linux frame buffer" 66 | # http://www.karoltomala.com/blog/?p=679 67 | DISPLAY = os.getenv("DISPLAY") 68 | if DISPLAY: 69 | print("Display: {0}".format(DISPLAY)) 70 | 71 | if os.getenv('SDL_VIDEODRIVER'): 72 | print("Using driver specified by SDL_VIDEODRIVER: {}".format(os.getenv('SDL_VIDEODRIVER'))) 73 | pygame.display.init() 74 | size = (pygame.display.Info().current_w, pygame.display.Info().current_h) 75 | if size == (480, 480): # Fix for 480x480 mode offset 76 | size = (640, 480) 77 | self.screen = pygame.display.set_mode(size, pygame.FULLSCREEN | pygame.DOUBLEBUF | pygame.NOFRAME | pygame.HWSURFACE) 78 | return 79 | 80 | else: 81 | # Iterate through drivers and attempt to init/set_mode 82 | for driver in ['rpi', 'kmsdrm', 'fbcon', 'directfb', 'svgalib']: 83 | os.putenv('SDL_VIDEODRIVER', driver) 84 | try: 85 | pygame.display.init() 86 | size = (pygame.display.Info().current_w, pygame.display.Info().current_h) 87 | if size == (480, 480): # Fix for 480x480 mode offset 88 | size = (640, 480) 89 | self.screen = pygame.display.set_mode(size, pygame.FULLSCREEN | pygame.DOUBLEBUF | pygame.NOFRAME | pygame.HWSURFACE) 90 | print("Using driver: {0}, Framebuffer size: {1:d} x {2:d}".format(driver, *size)) 91 | return 92 | except pygame.error as e: 93 | print('Driver "{0}" failed: {1}'.format(driver, e)) 94 | continue 95 | break 96 | 97 | print("All SDL drivers failed, falling back to raw framebuffer access.") 98 | self._rawfb = True 99 | os.putenv('SDL_VIDEODRIVER', 'dummy') 100 | pygame.display.init() # Need to init for .convert() to work 101 | self.screen = pygame.Surface((480, 480)) 102 | 103 | def __del__(self): 104 | "Destructor to make sure pygame shuts down, etc." 105 | 106 | def _updatefb(self): 107 | fbdev = os.getenv('SDL_FBDEV', '/dev/fb0') 108 | with open(fbdev, 'wb') as fb: 109 | fb.write(self.screen.convert(16, 0).get_buffer()) 110 | 111 | def get_colour(self): 112 | return tuple([int(c * 255) for c in hsv_to_rgb(self._hue, 1.0, self._val)]) 113 | 114 | def touch(self, x, y, state): 115 | target = pygame.math.Vector2(x, y) 116 | distance = self._origin.distance_to(target) 117 | angle = pygame.Vector2().angle_to(self._origin - target) 118 | 119 | if distance < self.inner_radius and distance > self.inner_radius - 40: 120 | return 121 | 122 | angle %= 360 123 | angle /= 360.0 124 | 125 | if distance < self.inner_radius: 126 | self._val = angle 127 | else: 128 | self._hue = angle 129 | 130 | # print("Displaying #{0:02x}{1:02x}{2:02x} {3}".format(*self.get_colour(), angle)) 131 | 132 | def run(self): 133 | self._running = True 134 | signal.signal(signal.SIGINT, self._exit) 135 | while self._running: 136 | for event in pygame.event.get(): 137 | if event.type == pygame.QUIT: 138 | self._running = False 139 | break 140 | if event.type == pygame.KEYDOWN: 141 | if event.key == pygame.K_ESCAPE: 142 | self._running = False 143 | break 144 | 145 | self._colour = tuple([int(c * 255) for c in hsv_to_rgb(self._hue, 1.0, self._val)]) 146 | pygame.draw.circle(self.screen, self.get_colour(), self.center, self.inner_radius - 10) 147 | pygame.draw.circle(self.screen, (0, 0, 0), self.center, self.inner_radius - 30) 148 | for s in range(360 * 3): 149 | a = s / 3.0 150 | cos = math.cos(math.radians(a)) 151 | sin = math.sin(math.radians(a)) 152 | x, y = self.center 153 | ox = x - (self.inner_radius - 40) * cos 154 | oy = y - (self.inner_radius - 40) * sin 155 | colour = tuple([int(c * 255) for c in hsv_to_rgb(self._hue, 1.0, a / 360.0)]) 156 | pygame.draw.line(self.screen, colour, (ox, oy), (x, y), 3) 157 | 158 | if self._rawfb: 159 | self._updatefb() 160 | else: 161 | pygame.display.flip() 162 | self._clock.tick(30) 163 | 164 | pygame.quit() 165 | sys.exit(0) 166 | 167 | 168 | display = Hyperpixel2r() 169 | touch = Touch() 170 | 171 | # uncomment to set up rgbmatrix 172 | # rgbmatrix = rgbmatrix5x5.RGBMatrix5x5(i2c_dev=touch._bus) 173 | # rgbmatrix.set_clear_on_exit() 174 | 175 | 176 | @touch.on_touch 177 | def handle_touch(touch_id, x, y, state): 178 | display.touch(x, y, state) 179 | # uncomment to set colour on rgbmatrix, 180 | # or try it with Mote USB or something! 181 | # rgbmatrix.set_all(*display.get_colour()) 182 | # rgbmatrix.show() 183 | 184 | 185 | display.run() 186 | -------------------------------------------------------------------------------- /examples/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import pygame 4 | import time 5 | import signal 6 | import math 7 | from colorsys import hsv_to_rgb 8 | from hyperpixel2r import Touch 9 | 10 | 11 | """ 12 | HyperPixel 2 Test 13 | 14 | Run with: sudo SDL_FBDEV=/dev/fb0 python3 test.py 15 | """ 16 | 17 | 18 | class Hyperpixel2r: 19 | screen = None 20 | 21 | def __init__(self): 22 | self._init_display() 23 | 24 | self.screen.fill((0, 0, 0)) 25 | self._updatefb() 26 | 27 | self._step = 0 28 | self._steps = [ 29 | (255, 0, 0, 240, 100), # Top 30 | (0, 255, 0, 240, 380), # Bottom 31 | (255, 0, 0, 100, 240), # Left 32 | (0, 255, 0, 380, 240), # Right 33 | (0, 0, 255, 240, 240), # Middle 34 | ] 35 | self._touched = False 36 | 37 | def _init_display(self): 38 | self._rawfb = False 39 | # Based on "Python GUI in Linux frame buffer" 40 | # http://www.karoltomala.com/blog/?p=679 41 | DISPLAY = os.getenv("DISPLAY") 42 | if DISPLAY: 43 | print("Display: {0}".format(DISPLAY)) 44 | 45 | if os.getenv('SDL_VIDEODRIVER'): 46 | print("Using driver specified by SDL_VIDEODRIVER: {}".format(os.getenv('SDL_VIDEODRIVER'))) 47 | pygame.display.init() 48 | size = (pygame.display.Info().current_w, pygame.display.Info().current_h) 49 | if size == (480, 480): # Fix for 480x480 mode offset 50 | size = (640, 480) 51 | self.screen = pygame.display.set_mode(size, pygame.FULLSCREEN | pygame.DOUBLEBUF | pygame.NOFRAME | pygame.HWSURFACE) 52 | return 53 | 54 | else: 55 | # Iterate through drivers and attempt to init/set_mode 56 | for driver in ['rpi', 'kmsdrm', 'fbcon', 'directfb', 'svgalib']: 57 | os.putenv('SDL_VIDEODRIVER', driver) 58 | try: 59 | pygame.display.init() 60 | size = (pygame.display.Info().current_w, pygame.display.Info().current_h) 61 | if size == (480, 480): # Fix for 480x480 mode offset 62 | size = (640, 480) 63 | self.screen = pygame.display.set_mode(size, pygame.FULLSCREEN | pygame.DOUBLEBUF | pygame.NOFRAME | pygame.HWSURFACE) 64 | print("Using driver: {0}, Framebuffer size: {1:d} x {2:d}".format(driver, *size)) 65 | return 66 | except pygame.error as e: 67 | print('Driver "{0}" failed: {1}'.format(driver, e)) 68 | continue 69 | break 70 | 71 | print("All SDL drivers failed, falling back to raw framebuffer access.") 72 | self._rawfb = True 73 | os.putenv('SDL_VIDEODRIVER', 'dummy') 74 | pygame.display.init() # Need to init for .convert() to work 75 | self.screen = pygame.Surface((480, 480)) 76 | 77 | def __del__(self): 78 | "Destructor to make sure pygame shuts down, etc." 79 | 80 | def _updatefb(self): 81 | if not self._rawfb: 82 | pygame.display.update() 83 | return 84 | 85 | fbdev = os.getenv('SDL_FBDEV', '/dev/fb0') 86 | with open(fbdev, 'wb') as fb: 87 | fb.write(self.screen.convert(16, 0).get_buffer()) 88 | 89 | def touch(self, x, y, state): 90 | if state: 91 | _, _, _, tx, ty = self._steps[self._step] 92 | x = abs(tx - x) 93 | y = abs(ty - y) 94 | distance = math.sqrt(x**2 + y**2) 95 | if distance < 90: 96 | self._touched = True 97 | 98 | def test(self, timeout=2): 99 | for colour in [(255, 255, 255), (255, 0, 0), (0, 255, 0), (0, 0, 255), (0, 0, 0)]: 100 | self.screen.fill(colour) 101 | print("Displaying #{0:02x}{1:02x}{2:02x}".format(*colour)) 102 | self._updatefb() 103 | time.sleep(0.25) 104 | 105 | for y in range(480): 106 | hue = y / 480.0 107 | colour = tuple([int(c * 255) for c in hsv_to_rgb(hue, 1.0, 1.0)]) 108 | pygame.draw.line(self.screen, colour, (0, y), (479, y)) 109 | 110 | self._updatefb() 111 | time.sleep(1.0) 112 | 113 | while self._step < len(self._steps): 114 | r, g, b, x, y = self._steps[self._step] 115 | pygame.draw.circle(self.screen, (r, g, b), (x, y), 90) 116 | self._updatefb() 117 | t_start = time.time() 118 | while not self._touched: 119 | if time.time() - t_start > timeout: 120 | raise RuntimeError("Touch test timed out!") 121 | self._touched = False 122 | pygame.draw.circle(self.screen, (0, 0, 0), (x, y), 90) 123 | 124 | self._updatefb() 125 | 126 | self._step += 1 127 | 128 | self.screen.fill((0, 0, 0)) 129 | self._updatefb() 130 | 131 | 132 | display = Hyperpixel2r() 133 | touch = Touch() 134 | 135 | @touch.on_touch 136 | def handle_touch(touch_id, x, y, state): 137 | display.touch(x, y, state) 138 | 139 | 140 | display.test() 141 | -------------------------------------------------------------------------------- /examples/uinput-touch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import signal 5 | import sys 6 | import time 7 | 8 | from datetime import datetime 9 | from threading import Timer 10 | 11 | try: 12 | from evdev import uinput, UInput, AbsInfo, ecodes as e 13 | except ImportError: 14 | exit("This service requires the evdev module\nInstall with: sudo pip install evdev") 15 | 16 | try: 17 | import RPi.GPIO as gpio 18 | except ImportError: 19 | exit("This service requires the RPi.GPIO module\nInstall with: sudo pip install RPi.GPIO") 20 | 21 | try: 22 | import smbus 23 | except ImportError: 24 | exit("This service requires the smbus module\nInstall with: sudo apt-get install python-smbus") 25 | 26 | 27 | os.system("sudo modprobe uinput") 28 | 29 | rotate = False 30 | 31 | try: 32 | config = open("/boot/config.txt").read().split("\n") 33 | for option in config: 34 | if option.startswith("display_rotate="): 35 | key, value = option.split("=") 36 | if value.strip() == "0": 37 | rotate = True 38 | except IOError: 39 | pass 40 | 41 | DAEMON = False 42 | 43 | CAPABILITIES = { 44 | e.EV_ABS : ( 45 | (e.ABS_X, AbsInfo(value=0, min=0, max=480, fuzz=0, flat=0, resolution=1)), 46 | (e.ABS_Y, AbsInfo(value=0, min=0, max=480, fuzz=0, flat=0, resolution=1)), 47 | (e.ABS_MT_SLOT, AbsInfo(value=0, min=0, max=1, fuzz=0, flat=0, resolution=0)), 48 | (e.ABS_MT_TRACKING_ID, AbsInfo(value=0, min=0, max=65535, fuzz=0, flat=0, resolution=0)), 49 | (e.ABS_MT_POSITION_X, AbsInfo(value=0, min=0, max=480, fuzz=0, flat=0, resolution=0)), 50 | (e.ABS_MT_POSITION_Y, AbsInfo(value=0, min=0, max=480, fuzz=0, flat=0, resolution=0)), 51 | ), 52 | e.EV_KEY : [ 53 | e.BTN_TOUCH, 54 | ] 55 | } 56 | 57 | PIDFILE = "/var/run/hyperpixel2r-touch.pid" 58 | LOGFILE = "/var/log/hyperpixel2r-touch.log" 59 | 60 | if DAEMON: 61 | try: 62 | pid = os.fork() 63 | if pid > 0: 64 | sys.exit(0) 65 | 66 | except OSError as e: 67 | print("Fork #1 failed: {} ({})".format(e.errno, e.strerror)) 68 | sys.exit(1) 69 | 70 | os.chdir("/") 71 | os.setsid() 72 | os.umask(0) 73 | 74 | try: 75 | pid = os.fork() 76 | if pid > 0: 77 | fpid = open(PIDFILE, 'w') 78 | fpid.write(str(pid)) 79 | fpid.close() 80 | sys.exit(0) 81 | except OSError as e: 82 | print("Fork #2 failed: {} ({})".format(e.errno, e.strerror)) 83 | sys.exit(1) 84 | 85 | si = file("/dev/null", 'r') 86 | so = file(LOGFILE, 'a+') 87 | se = file("/dev/null", 'a+', 0) 88 | 89 | os.dup2(si.fileno(), sys.stdin.fileno()) 90 | os.dup2(so.fileno(), sys.stdout.fileno()) 91 | os.dup2(se.fileno(), sys.stderr.fileno()) 92 | 93 | def log(msg): 94 | sys.stdout.write(str(datetime.now())) 95 | sys.stdout.write(": ") 96 | sys.stdout.write(msg) 97 | sys.stdout.write("\n") 98 | sys.stdout.flush() 99 | 100 | try: 101 | ui = UInput(CAPABILITIES, name="Touchscreen", bustype=e.BUS_USB) 102 | 103 | except uinput.UInputError as e: 104 | sys.stdout.write(e.message) 105 | sys.stdout.write("Have you tried running as root? sudo {}".format(sys.argv[0])) 106 | sys.exit(0) 107 | 108 | 109 | from hyperpixel2r import Touch 110 | 111 | 112 | last_status = [False, False] 113 | last_status_xy = [(0, 0), (0, 0)] 114 | 115 | 116 | def write_status(touch_id, x, y, touch_state): 117 | if touch_id > 1: # Support touches 0 and 1 118 | return 119 | 120 | if touch_state: 121 | ui.write(e.EV_ABS, e.ABS_MT_SLOT, touch_id) 122 | 123 | if not last_status[touch_id]: # Contact one press 124 | ui.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, touch_id) 125 | ui.write(e.EV_ABS, e.ABS_MT_POSITION_X, x) 126 | ui.write(e.EV_ABS, e.ABS_MT_POSITION_Y, y) 127 | ui.write(e.EV_KEY, e.BTN_TOUCH, 1) 128 | ui.write(e.EV_ABS, e.ABS_X, x) 129 | ui.write(e.EV_ABS, e.ABS_Y, y) 130 | 131 | elif not last_status[touch_id] or (x, y) != last_status_xy[touch_id]: 132 | if x != last_status_xy[touch_id][0]: ui.write(e.EV_ABS, e.ABS_X, x) 133 | if y != last_status_xy[touch_id][1]: ui.write(e.EV_ABS, e.ABS_Y, y) 134 | ui.write(e.EV_ABS, e.ABS_MT_POSITION_X, x) 135 | ui.write(e.EV_ABS, e.ABS_MT_POSITION_Y, y) 136 | 137 | last_status_xy[touch_id] = (x, y) 138 | last_status[touch_id] = True 139 | 140 | ui.write(e.EV_SYN, e.SYN_REPORT, 0) 141 | ui.syn() 142 | 143 | elif not touch_state and last_status[touch_id]: 144 | ui.write(e.EV_ABS, e.ABS_MT_SLOT, touch_id) 145 | ui.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, -1) 146 | ui.write(e.EV_KEY, e.BTN_TOUCH, 0) 147 | last_status[touch_id] = False 148 | 149 | ui.write(e.EV_SYN, e.SYN_REPORT, 0) 150 | ui.syn() 151 | 152 | 153 | log("HyperPixel2r Touch daemon running...") 154 | 155 | touch = Touch() 156 | 157 | 158 | @touch.on_touch 159 | def handle_touch(touch_id, x, y, state): 160 | write_status(touch_id, x, y, state) 161 | 162 | 163 | signal.pause() 164 | 165 | log("HyperPixel2r Touch daemon shutting down...") 166 | 167 | ui.close() 168 | -------------------------------------------------------------------------------- /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 = hyperpixel2r 3 | omit = 4 | .tox/* 5 | -------------------------------------------------------------------------------- /library/CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 0.0.1 2 | ----- 3 | 4 | * Initial Release 5 | -------------------------------------------------------------------------------- /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 hyperpixel2r *.py 6 | -------------------------------------------------------------------------------- /library/README.md: -------------------------------------------------------------------------------- 1 | # HyperPixel 2" Round Touch Driver 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/pimoroni/hyperpixel2r-python/Python%20Tests)](https://github.com/pimoroni/hyperpixel2r-python/actions/workflows/test.yml) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/hyperpixel2r-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/hyperpixel2r-python?branch=master) 5 | [![PyPi Package](https://img.shields.io/pypi/v/hyperpixel2r.svg)](https://pypi.python.org/pypi/hyperpixel2r) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/hyperpixel2r.svg)](https://pypi.python.org/pypi/hyperpixel2r) 7 | 8 | # Pre-requisites 9 | 10 | You must install the HyperPixel 2r drivers which enable an i2c bus for the touch IC - https://github.com/pimoroni/hyperpixel4/tree/hp2-round 11 | 12 | # Installing 13 | 14 | Stable library from PyPi: 15 | 16 | * Just run `pip3 install hyperpixel2r` 17 | 18 | In some cases you may need to use `sudo` or install pip with: `sudo apt install python3-pip` 19 | 20 | Latest/development library from GitHub: 21 | 22 | * `git clone https://github.com/pimoroni/hyperpixel2r-python` 23 | * `cd hyperpixel2r-python` 24 | * `sudo ./install.sh` 25 | 26 | # Usage 27 | 28 | Set up touch driver instance: 29 | 30 | ```python 31 | touch = Touch(bus=11, i2c_addr=0x15, interrupt_pin=27): 32 | ``` 33 | 34 | Touches should be read by decorating a handler with `@touch.on_touch`. 35 | 36 | The handler should accept the arguments `touch_id`, `x`, `y` and `state`. 37 | 38 | * `touch_id` - 0 or 1 depending on which touch is tracked 39 | * `x` - x coordinate from 0 to 479 40 | * `y` - y coordinate from 0 to 479 41 | * `state` - touch state `True` for touched, `False` for released 42 | 43 | For example: 44 | 45 | ```python 46 | @touch.on_touch 47 | def handle_touch(touch_id, x, y, state): 48 | print(touch_id, x, y, state) 49 | ``` 50 | 51 | # Changelog 52 | 0.0.1 53 | ----- 54 | 55 | * Initial Release 56 | -------------------------------------------------------------------------------- /library/hyperpixel2r/__init__.py: -------------------------------------------------------------------------------- 1 | import smbus2 2 | import struct 3 | import RPi.GPIO as GPIO 4 | 5 | 6 | __version__ = '0.0.1' 7 | 8 | 9 | class Touch: 10 | def __init__(self, bus=11, i2c_addr=0x15, interrupt_pin=27): 11 | self._i2c_addr = i2c_addr 12 | self._interrupt_pin = interrupt_pin 13 | self._bus = smbus2.SMBus(bus) 14 | self._callback_handler = None 15 | self._touches = {} 16 | 17 | GPIO.setmode(GPIO.BCM) 18 | GPIO.setup(self._interrupt_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) 19 | GPIO.add_event_detect(self._interrupt_pin, edge=GPIO.FALLING, callback=self._handle_interrupt, bouncetime=1) 20 | 21 | def on_touch(self, handler): 22 | self._callback_handler = handler 23 | 24 | def _handle_interrupt(self, pin): 25 | count = self._bus.read_byte_data(self._i2c_addr, 0x02) 26 | # We don't get release events unless we always read both touches 27 | count = 2 28 | if count > 0: 29 | data = self._bus.read_i2c_block_data(self._i2c_addr, 0x03, count * 6) 30 | for i in range(count): 31 | offset = i * 6 32 | touch_status = False 33 | touch = data[offset:offset + 6] 34 | touch_event = touch[0] & 0xf0 35 | touch_id = (touch[2] & 0xf0) >> 4 36 | touch[0] &= 0x0f # Mask out event_flg 37 | touch[2] &= 0x0f # Mask out touch_ID 38 | tx, ty, p1, p2 = struct.unpack(">HHBB", bytes(touch)) 39 | 40 | if touch_event & 128: 41 | touch_status = True 42 | 43 | if touch_event & 64: 44 | touch_status = False 45 | 46 | new_touch = touch_id, tx, ty, touch_status 47 | 48 | current_touch = self._touches.get(touch_id, None) 49 | 50 | if new_touch != current_touch: 51 | self._touches[touch_id] = new_touch 52 | if callable(self._callback_handler): 53 | self._callback_handler(*self._touches[touch_id]) 54 | -------------------------------------------------------------------------------- /library/hyperpixel2r/__main__.py: -------------------------------------------------------------------------------- 1 | import signal 2 | 3 | from . import Touch 4 | 5 | 6 | if __name__ == "__main__": 7 | touch = Touch() 8 | 9 | print("HyperPixel 2 Round: Touch Test") 10 | 11 | @touch.on_touch 12 | def handle_touch(touch_id, x, y, state): 13 | print(touch_id, x, y, state) 14 | 15 | signal.pause() 16 | -------------------------------------------------------------------------------- /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 = hyperpixel2r 4 | version = 0.0.1 5 | author = Philip Howard 6 | author_email = phil@pimoroni.com 7 | description = Python driver for the HyperPixel 2" round LCD 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/hyperpixel2r-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 = hyperpixel2r 32 | install_requires = 33 | smbus2 34 | 35 | [flake8] 36 | exclude = 37 | .tox, 38 | .eggs, 39 | .git, 40 | __pycache__, 41 | build, 42 | dist 43 | ignore = 44 | E501 45 | 46 | [pimoroni] 47 | py2deps = 48 | py3deps = 49 | configtxt = 50 | commands = 51 | -------------------------------------------------------------------------------- /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 smbus2(): 8 | """Mock smbus module.""" 9 | smbus = mock.MagicMock() 10 | sys.modules['smbus2'] = smbus 11 | yield smbus 12 | del sys.modules['smbus2'] 13 | 14 | 15 | @pytest.fixture(scope='function', autouse=False) 16 | def GPIO(): 17 | """Mock RPi.GPIO module.""" 18 | 19 | GPIO = mock.MagicMock() 20 | # Fudge for Python < 37 (possibly earlier) 21 | sys.modules['RPi'] = mock.Mock() 22 | sys.modules['RPi'].GPIO = GPIO 23 | sys.modules['RPi.GPIO'] = GPIO 24 | yield GPIO 25 | del sys.modules['RPi'] 26 | del sys.modules['RPi.GPIO'] 27 | -------------------------------------------------------------------------------- /library/tests/test_setup.py: -------------------------------------------------------------------------------- 1 | import mock 2 | 3 | 4 | def test_setup(smbus2, GPIO): 5 | from hyperpixel2r import Touch 6 | 7 | touch = Touch() 8 | 9 | GPIO.setmode.assert_has_calls(( 10 | mock.call(GPIO.BCM), 11 | )) 12 | 13 | GPIO.setup.assert_has_calls(( 14 | mock.call(touch._interrupt_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP), 15 | )) 16 | 17 | del touch 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------