/ 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