/ adafruit_slideshow.py
adafruit_slideshow.py
1 # The MIT License (MIT) 2 # 3 # Copyright (c) 2018 Kattni Rembor for Adafruit Industries 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 13 # all 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 21 # THE SOFTWARE. 22 23 """ 24 `adafruit_slideshow` 25 ==================================================== 26 CircuitPython helper library for displaying a slideshow of images on a display. 27 28 * Author(s): Kattni Rembor, Carter Nelson, Roy Hooper 29 30 Implementation Notes 31 -------------------- 32 33 **Hardware:** 34 35 * `Adafruit Hallowing M0 Express <https://www.adafruit.com/product/3900>`_ 36 37 **Software and Dependencies:** 38 39 * Adafruit CircuitPython firmware for the supported boards: 40 https://github.com/adafruit/circuitpython/releases 41 42 """ 43 import time 44 import os 45 import random 46 import displayio 47 48 __version__ = "0.0.0-auto.0" 49 __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Slideshow.git" 50 51 52 class PlayBackOrder: 53 """Defines possible slideshow playback orders.""" 54 55 # pylint: disable=too-few-public-methods 56 ALPHABETICAL = 0 57 """Orders by alphabetical sort of filenames""" 58 59 RANDOM = 1 60 """Randomly shuffles the images""" 61 # pylint: enable=too-few-public-methods 62 63 64 class PlayBackDirection: 65 """Defines possible slideshow playback directions.""" 66 67 # pylint: disable=too-few-public-methods 68 BACKWARD = -1 69 """The next image is before the current image. When alphabetically sorted, this is towards A.""" 70 71 FORWARD = 1 72 """The next image is after the current image. When alphabetically sorted, this is towards Z.""" 73 # pylint: enable=too-few-public-methods 74 75 76 class SlideShow: 77 # pylint: disable=too-many-instance-attributes 78 """ 79 Class for displaying a slideshow of .bmp images on displays. 80 81 :param str folder: Specify the folder containing the image files, in quotes. Default is 82 the root directory, ``"/"``. 83 84 :param PlayBackOrder order: The order in which the images display. You can choose random 85 (``RANDOM``) or alphabetical (``ALPHABETICAL``). Default is 86 ``ALPHABETICAL``. 87 88 :param bool loop: Specify whether to loop the images or play through the list once. `True` 89 if slideshow will continue to loop, ``False`` if it will play only once. 90 Default is ``True``. 91 92 :param int dwell: The number of seconds each image displays, in seconds. Default is 3. 93 94 :param bool fade_effect: Specify whether to include the fade effect between images. ``True`` 95 tells the code to fade the backlight up and down between image display 96 transitions. ``False`` maintains max brightness on the backlight between 97 image transitions. Default is ``True``. 98 99 :param bool auto_advance: Specify whether to automatically advance after dwell seconds. ``True`` 100 if slideshow should auto play, ``False`` if you want to control advancement 101 manually. Default is ``True``. 102 103 :param PlayBackDirection direction: The playback direction. 104 105 Example code for Hallowing Express. With this example, the slideshow will play through once 106 in alphabetical order: 107 108 .. code-block:: python 109 110 from adafruit_slideshow import PlayBackOrder, SlideShow 111 import board 112 import pulseio 113 114 slideshow = SlideShow(board.DISPLAY, pulseio.PWMOut(board.TFT_BACKLIGHT), folder="/", 115 loop=False, order=PlayBackOrder.ALPHABETICAL) 116 117 while slideshow.update(): 118 pass 119 120 Example code for Hallowing Express. Sets ``dwell`` to 0 seconds, turns ``auto_advance`` off, 121 and uses capacitive touch to advance backwards and forwards through the images and to control 122 the brightness level of the backlight: 123 124 .. code-block:: python 125 126 from adafruit_slideshow import PlayBackOrder, SlideShow, PlayBackDirection 127 import touchio 128 import board 129 import pulseio 130 131 forward_button = touchio.TouchIn(board.TOUCH4) 132 back_button = touchio.TouchIn(board.TOUCH1) 133 134 brightness_up = touchio.TouchIn(board.TOUCH3) 135 brightness_down = touchio.TouchIn(board.TOUCH2) 136 137 slideshow = SlideShow(board.DISPLAY, pulseio.PWMOut(board.TFT_BACKLIGHT), folder="/", 138 auto_advance=False, dwell=0) 139 140 while True: 141 if forward_button.value: 142 slideshow.direction = PlayBackDirection.FORWARD 143 slideshow.advance() 144 if back_button.value: 145 slideshow.direction = PlayBackDirection.BACKWARD 146 slideshow.advance() 147 148 if brightness_up.value: 149 slideshow.brightness += 0.001 150 elif brightness_down.value: 151 slideshow.brightness -= 0.001 152 """ 153 154 def __init__( 155 self, 156 display, 157 backlight_pwm=None, 158 *, 159 folder="/", 160 order=PlayBackOrder.ALPHABETICAL, 161 loop=True, 162 dwell=3, 163 fade_effect=True, 164 auto_advance=True, 165 direction=PlayBackDirection.FORWARD 166 ): 167 self.loop = loop 168 """Specifies whether to loop through the images continuously or play through the list once. 169 ``True`` will continue to loop, ``False`` will play only once.""" 170 171 self.dwell = dwell 172 """The number of seconds each image displays, in seconds.""" 173 174 self.direction = direction 175 """Specify the playback direction. Default is ``PlayBackDirection.FORWARD``. Can also be 176 ``PlayBackDirection.BACKWARD``.""" 177 178 self.auto_advance = auto_advance 179 """Enable auto-advance based on dwell time. Set to ``False`` to manually control.""" 180 181 self.fade_effect = fade_effect 182 """Whether to include the fade effect between images. ``True`` tells the code to fade the 183 backlight up and down between image display transitions. ``False`` maintains max 184 brightness on the backlight between image transitions.""" 185 186 # Load the image names before setting order so they can be reordered. 187 self._img_start = None 188 self._file_list = [ 189 folder + "/" + f 190 for f in os.listdir(folder) 191 if (f.endswith(".bmp") and not f.startswith(".")) 192 ] 193 194 self._order = None 195 self.order = order 196 """The order in which the images display. You can choose random (``RANDOM``) or 197 alphabetical (``ALPHA``).""" 198 199 self._current_image = -1 200 self._image_file = None 201 self._brightness = 0.5 202 # 4.0.0 Beta 2 replaces Sprite with TileGrid so use either. 203 self._sprite_class = getattr(displayio, "Sprite", displayio.TileGrid) 204 205 # Setup the display 206 self._group = displayio.Group() 207 self._display = display 208 display.show(self._group) 209 210 self._backlight_pwm = backlight_pwm 211 if not backlight_pwm and fade_effect: 212 self._display.auto_brightness = False 213 214 # Show the first image 215 self.advance() 216 217 @property 218 def current_image_name(self): 219 """Returns the current image name.""" 220 return self._file_list[self._current_image] 221 222 @property 223 def order(self): 224 """Specifies the order in which the images are displayed. Options are random (``RANDOM``) or 225 alphabetical (``ALPHABETICAL``). Default is ``RANDOM``.""" 226 return self._order 227 228 @order.setter 229 def order(self, order): 230 if order not in [PlayBackOrder.ALPHABETICAL, PlayBackOrder.RANDOM]: 231 raise ValueError("Order must be either 'RANDOM' or 'ALPHABETICAL'") 232 233 self._order = order 234 self._reorder_images() 235 236 def _reorder_images(self): 237 if self.order == PlayBackOrder.ALPHABETICAL: 238 self._file_list = sorted(self._file_list) 239 elif self.order == PlayBackOrder.RANDOM: 240 self._file_list = sorted(self._file_list, key=lambda x: random.random()) 241 242 def _set_backlight(self, brightness): 243 if self._backlight_pwm: 244 full_brightness = 2 ** 16 - 1 245 self._backlight_pwm.duty_cycle = int(full_brightness * brightness) 246 else: 247 try: 248 self._display.brightness = brightness 249 except RuntimeError: 250 pass 251 252 @property 253 def brightness(self): 254 """Brightness of the backlight when an image is displaying. Clamps to 0 to 1.0""" 255 return self._brightness 256 257 @brightness.setter 258 def brightness(self, brightness): 259 if brightness < 0: 260 brightness = 0 261 elif brightness > 1.0: 262 brightness = 1.0 263 self._brightness = brightness 264 self._set_backlight(brightness) 265 266 def _fade_up(self): 267 if not self.fade_effect: 268 self._set_backlight(self.brightness) 269 return 270 steps = 100 271 for i in range(steps): 272 self._set_backlight(self.brightness * i / steps) 273 time.sleep(0.01) 274 275 def _fade_down(self): 276 if not self.fade_effect: 277 self._set_backlight(self.brightness) 278 return 279 steps = 100 280 for i in range(steps, -1, -1): 281 self._set_backlight(self.brightness * i / steps) 282 time.sleep(0.01) 283 284 def update(self): 285 """Updates the slideshow to the next image.""" 286 now = time.monotonic() 287 if not self.auto_advance or now - self._img_start < self.dwell: 288 return True 289 290 return self.advance() 291 292 def advance(self): 293 """Displays the next image. Returns True when a new image was displayed, False otherwise. 294 """ 295 if self._image_file: 296 self._fade_down() 297 self._group.pop() 298 self._image_file.close() 299 self._image_file = None 300 301 self._current_image += self.direction 302 303 # Try and load an OnDiskBitmap until a valid file is found or we run out of options. This 304 # loop stops because we either set odb or reduce the length of _file_list. 305 odb = None 306 while not odb and self._file_list: 307 if 0 <= self._current_image < len(self._file_list): 308 pass 309 elif not self.loop: 310 return False 311 else: 312 image_count = len(self._file_list) 313 if self._current_image < 0: 314 self._current_image += image_count 315 elif self._current_image >= image_count: 316 self._current_image -= image_count 317 self._reorder_images() 318 319 image_name = self._file_list[self._current_image] 320 self._image_file = open(image_name, "rb") 321 try: 322 odb = displayio.OnDiskBitmap(self._image_file) 323 except ValueError: 324 self._image_file.close() 325 self._image_file = None 326 del self._file_list[self._current_image] 327 328 if not odb: 329 raise RuntimeError("No valid images") 330 331 try: 332 sprite = self._sprite_class(odb, pixel_shader=displayio.ColorConverter()) 333 except TypeError: 334 sprite = self._sprite_class( 335 odb, pixel_shader=displayio.ColorConverter(), position=(0, 0) 336 ) 337 self._group.append(sprite) 338 339 self._fade_up() 340 self._img_start = time.monotonic() 341 342 return True