rgb.py
  1  # The MIT License (MIT)
  2  #
  3  # Copyright (c) 2017 Radomir Dopieralski and 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  `adafruit_rgb_display.rgb`
 24  ====================================================
 25  
 26  Base class for all RGB Display devices
 27  
 28  * Author(s): Radomir Dopieralski, Michael McWethy
 29  """
 30  
 31  import time
 32  
 33  try:
 34      import numpy
 35  except ImportError:
 36      numpy = None
 37  try:
 38      import struct
 39  except ImportError:
 40      import ustruct as struct
 41  
 42  import adafruit_bus_device.spi_device as spi_device
 43  
 44  __version__ = "0.0.0-auto.0"
 45  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_RGB_Display.git"
 46  
 47  # This is the size of the buffer to be used for fill operations, in 16-bit
 48  # units.
 49  _BUFFER_SIZE = 256
 50  try:
 51      import platform
 52  
 53      if "CPython" in platform.python_implementation():
 54          _BUFFER_SIZE = 320 * 240  # blit the whole thing
 55  except ImportError:
 56      pass
 57  
 58  
 59  def color565(r, g=0, b=0):
 60      """Convert red, green and blue values (0-255) into a 16-bit 565 encoding.  As
 61      a convenience this is also available in the parent adafruit_rgb_display
 62      package namespace."""
 63      try:
 64          r, g, b = r  # see if the first var is a tuple/list
 65      except TypeError:
 66          pass
 67      return (r & 0xF8) << 8 | (g & 0xFC) << 3 | b >> 3
 68  
 69  
 70  def image_to_data(image):
 71      """Generator function to convert a PIL image to 16-bit 565 RGB bytes."""
 72      # NumPy is much faster at doing this. NumPy code provided by:
 73      # Keith (https://www.blogger.com/profile/02555547344016007163)
 74      data = numpy.array(image.convert("RGB")).astype("uint16")
 75      color = (
 76          ((data[:, :, 0] & 0xF8) << 8)
 77          | ((data[:, :, 1] & 0xFC) << 3)
 78          | (data[:, :, 2] >> 3)
 79      )
 80      return numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
 81  
 82  
 83  class DummyPin:
 84      """Can be used in place of a ``DigitalInOut()`` when you don't want to skip it."""
 85  
 86      def deinit(self):
 87          """Dummy DigitalInOut deinit"""
 88  
 89      def switch_to_output(self, *args, **kwargs):
 90          """Dummy switch_to_output method"""
 91  
 92      def switch_to_input(self, *args, **kwargs):
 93          """Dummy switch_to_input method"""
 94  
 95      @property
 96      def value(self):
 97          """Dummy value DigitalInOut property"""
 98  
 99      @value.setter
100      def value(self, val):
101          pass
102  
103      @property
104      def direction(self):
105          """Dummy direction DigitalInOut property"""
106  
107      @direction.setter
108      def direction(self, val):
109          pass
110  
111      @property
112      def pull(self):
113          """Dummy pull DigitalInOut property"""
114  
115      @pull.setter
116      def pull(self, val):
117          pass
118  
119  
120  class Display:  # pylint: disable-msg=no-member
121      """Base class for all RGB display devices
122          :param width: number of pixels wide
123          :param height: number of pixels high
124      """
125  
126      _PAGE_SET = None
127      _COLUMN_SET = None
128      _RAM_WRITE = None
129      _RAM_READ = None
130      _X_START = 0  # pylint: disable=invalid-name
131      _Y_START = 0  # pylint: disable=invalid-name
132      _INIT = ()
133      _ENCODE_PIXEL = ">H"
134      _ENCODE_POS = ">HH"
135      _DECODE_PIXEL = ">BBB"
136  
137      def __init__(self, width, height, rotation):
138          self.width = width
139          self.height = height
140          if rotation not in (0, 90, 180, 270):
141              raise ValueError("Rotation must be 0/90/180/270")
142          self._rotation = rotation
143          self.init()
144  
145      def init(self):
146          """Run the initialization commands."""
147          for command, data in self._INIT:
148              self.write(command, data)
149  
150      # pylint: disable-msg=invalid-name,too-many-arguments
151      def _block(self, x0, y0, x1, y1, data=None):
152          """Read or write a block of data."""
153          self.write(
154              self._COLUMN_SET, self._encode_pos(x0 + self._X_START, x1 + self._X_START)
155          )
156          self.write(
157              self._PAGE_SET, self._encode_pos(y0 + self._Y_START, y1 + self._Y_START)
158          )
159          if data is None:
160              size = struct.calcsize(self._DECODE_PIXEL)
161              return self.read(self._RAM_READ, (x1 - x0 + 1) * (y1 - y0 + 1) * size)
162          self.write(self._RAM_WRITE, data)
163          return None
164  
165      # pylint: enable-msg=invalid-name,too-many-arguments
166  
167      def _encode_pos(self, x, y):
168          """Encode a postion into bytes."""
169          return struct.pack(self._ENCODE_POS, x, y)
170  
171      def _encode_pixel(self, color):
172          """Encode a pixel color into bytes."""
173          return struct.pack(self._ENCODE_PIXEL, color)
174  
175      def _decode_pixel(self, data):
176          """Decode bytes into a pixel color."""
177          return color565(*struct.unpack(self._DECODE_PIXEL, data))
178  
179      def pixel(self, x, y, color=None):
180          """Read or write a pixel at a given position."""
181          if color is None:
182              return self._decode_pixel(self._block(x, y, x, y))
183  
184          if 0 <= x < self.width and 0 <= y < self.height:
185              self._block(x, y, x, y, self._encode_pixel(color))
186          return None
187  
188      def image(self, img, rotation=None, x=0, y=0):
189          """Set buffer to value of Python Imaging Library image. The image should
190          be in 1 bit mode and a size not exceeding the display size when drawn at
191          the supplied origin."""
192          if rotation is None:
193              rotation = self.rotation
194          if not img.mode in ("RGB", "RGBA"):
195              raise ValueError("Image must be in mode RGB or RGBA")
196          if rotation not in (0, 90, 180, 270):
197              raise ValueError("Rotation must be 0/90/180/270")
198          if rotation != 0:
199              img = img.rotate(rotation, expand=True)
200          imwidth, imheight = img.size
201          if x + imwidth > self.width or y + imheight > self.height:
202              raise ValueError(
203                  "Image must not exceed dimensions of display ({0}x{1}).".format(
204                      self.width, self.height
205                  )
206              )
207          if numpy:
208              pixels = list(image_to_data(img))
209          else:
210              # Slower but doesn't require numpy
211              pixels = bytearray(imwidth * imheight * 2)
212              for i in range(imwidth):
213                  for j in range(imheight):
214                      pix = color565(img.getpixel((i, j)))
215                      pixels[2 * (j * imwidth + i)] = pix >> 8
216                      pixels[2 * (j * imwidth + i) + 1] = pix & 0xFF
217          self._block(x, y, x + imwidth - 1, y + imheight - 1, pixels)
218  
219      # pylint: disable-msg=too-many-arguments
220      def fill_rectangle(self, x, y, width, height, color):
221          """Draw a rectangle at specified position with specified width and
222          height, and fill it with the specified color."""
223          x = min(self.width - 1, max(0, x))
224          y = min(self.height - 1, max(0, y))
225          width = min(self.width - x, max(1, width))
226          height = min(self.height - y, max(1, height))
227          self._block(x, y, x + width - 1, y + height - 1, b"")
228          chunks, rest = divmod(width * height, _BUFFER_SIZE)
229          pixel = self._encode_pixel(color)
230          if chunks:
231              data = pixel * _BUFFER_SIZE
232              for _ in range(chunks):
233                  self.write(None, data)
234          self.write(None, pixel * rest)
235  
236      # pylint: enable-msg=too-many-arguments
237  
238      def fill(self, color=0):
239          """Fill the whole display with the specified color."""
240          self.fill_rectangle(0, 0, self.width, self.height, color)
241  
242      def hline(self, x, y, width, color):
243          """Draw a horizontal line."""
244          self.fill_rectangle(x, y, width, 1, color)
245  
246      def vline(self, x, y, height, color):
247          """Draw a vertical line."""
248          self.fill_rectangle(x, y, 1, height, color)
249  
250      @property
251      def rotation(self):
252          """Set the default rotation"""
253          return self._rotation
254  
255      @rotation.setter
256      def rotation(self, val):
257          if val not in (0, 90, 180, 270):
258              raise ValueError("Rotation must be 0/90/180/270")
259          self._rotation = val
260  
261  
262  class DisplaySPI(Display):
263      """Base class for SPI type devices"""
264  
265      # pylint: disable-msg=too-many-arguments
266      def __init__(
267          self,
268          spi,
269          dc,
270          cs,
271          rst=None,
272          width=1,
273          height=1,
274          baudrate=12000000,
275          polarity=0,
276          phase=0,
277          *,
278          x_offset=0,
279          y_offset=0,
280          rotation=0
281      ):
282          self.spi_device = spi_device.SPIDevice(
283              spi, cs, baudrate=baudrate, polarity=polarity, phase=phase
284          )
285          self.dc_pin = dc
286          self.rst = rst
287          self.dc_pin.switch_to_output(value=0)
288          if self.rst:
289              self.rst.switch_to_output(value=0)
290              self.reset()
291          self._X_START = x_offset  # pylint: disable=invalid-name
292          self._Y_START = y_offset  # pylint: disable=invalid-name
293          super().__init__(width, height, rotation)
294  
295      # pylint: enable-msg=too-many-arguments
296  
297      def reset(self):
298          """Reset the device"""
299          self.rst.value = 0
300          time.sleep(0.050)  # 50 milliseconds
301          self.rst.value = 1
302          time.sleep(0.050)  # 50 milliseconds
303  
304      # pylint: disable=no-member
305      def write(self, command=None, data=None):
306          """SPI write to the device: commands and data"""
307          if command is not None:
308              self.dc_pin.value = 0
309              with self.spi_device as spi:
310                  spi.write(bytearray([command]))
311          if data is not None:
312              self.dc_pin.value = 1
313              with self.spi_device as spi:
314                  spi.write(data)
315  
316      def read(self, command=None, count=0):
317          """SPI read from device with optional command"""
318          data = bytearray(count)
319          self.dc_pin.value = 0
320          with self.spi_device as spi:
321              if command is not None:
322                  spi.write(bytearray([command]))
323              if count:
324                  spi.readinto(data)
325          return data