/ adafruit_rgb_display / rgb.py
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