]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio.py
Basic functionality in place
[hackapet/Adafruit_Blinka_Displayio.git] / displayio.py
1 """
2 `displayio`
3 """
4
5 import os
6 import digitalio
7 import time
8 import struct
9 import threading
10 import numpy
11 from collections import namedtuple
12 from PIL import Image, ImageDraw, ImagePalette
13
14 """
15 import asyncio
16 import signal
17 import subprocess
18 """
19
20 # Don't import pillow if we're running in the CI. We could mock it out but that
21 # would require mocking in all reverse dependencies.
22 if "GITHUB_ACTION" not in os.environ and "READTHEDOCS" not in os.environ:
23     # This will only work on Linux
24     pass
25 else:
26     # this would be for Github Actions
27     utils = None  # pylint: disable=invalid-name
28
29 __version__ = "0.0.0-auto.0"
30 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
31
32 _displays = []
33 _groups = []
34
35 Rectangle = namedtuple("Rectangle", "x1 y1 x2 y2")
36
37 class _DisplayioSingleton:
38     def __init__(self):
39         pass
40
41
42 def release_displays():
43     """Releases any actively used displays so their busses and pins can be used again.
44
45     Use this once in your code.py if you initialize a display. Place it right before the initialization so the display is active as long as possible.
46     """
47     for _disp in _displays:
48         _disp._release()
49     _displays.clear()
50
51 class Bitmap:
52     """Stores values of a certain size in a 2D array"""
53
54     def __init__(self, width, height, value_count):
55         """Create a Bitmap object with the given fixed size. Each pixel stores a value that is used to index into a corresponding palette. This enables differently colored sprites to share the underlying Bitmap. value_count is used to minimize the memory used to store the Bitmap.
56         """
57         self._width = width
58         self._height = height
59         self._read_only = False
60
61         if value_count < 0:
62             raise ValueError("value_count must be > 0")
63
64         bits = 1
65         while (value_count - 1) >> bits:
66             if bits < 8:
67                 bits = bits << 1
68             else:
69                 bits += 8
70
71         self._bits_per_value = bits
72
73         if (
74             self._bits_per_value > 8
75             and self._bits_per_value != 16
76             and self._bits_per_value != 32
77         ):
78             raise NotImplementedError("Invalid bits per value")
79
80         self._data = (width * height) * [0]
81         self._dirty_area = Rectangle(0, 0, width, height)
82
83     def __getitem__(self, index):
84         """
85         Returns the value at the given index. The index can either be
86         an x,y tuple or an int equal to `y * width + x`.
87         """
88         if isinstance(index, (tuple, list)):
89             index = index[1] * self._width + index[0]
90         return self._data[index]
91
92     def __setitem__(self, index, value):
93         """
94         Sets the value at the given index. The index can either be
95         an x,y tuple or an int equal to `y * width + x`.
96         """
97         if self._read_only:
98             raise RuntimeError("Read-only object")
99         if isinstance(index, (tuple, list)):
100             x = index[0]
101             y = index[1]
102             index = y * self._width + x
103         elif ininstance(index, int):
104             x = index % self._width
105             y = index // self._width
106         self._data[index] = value
107         if self._dirty_area.x1 == self._dirty_area.x2:
108             self._dirty_area.x1 = x
109             self._dirty_area.x2 = x + 1
110             self._dirty_area.y1 = y
111             self._dirty_area.y2 = y + 1
112         else:
113             if x < self._dirty_area.x1:
114                 self._dirty_area.x1 = x
115             elif x >= self._dirty_area.x2:
116                 self._dirty_area.x2 = x + 1
117             if y < self._dirty_area.y1:
118                 self._dirty_area.y1 = y
119             elif y >= self._dirty_area.y2:
120                 self._dirty_area.y2 = y + 1
121
122     def _finish_refresh(self):
123         self._dirty_area.x1 = 0
124         self._dirty_area.x2 = 0
125
126     def fill(self, value):
127         """Fills the bitmap with the supplied palette index value."""
128         self._data = (self._width * self._height) * [value]
129         self._dirty_area = Rectangle(0, 0, self._width, self._height)
130
131     @property
132     def width(self):
133         """Width of the bitmap. (read only)"""
134         return self._width
135
136     @property
137     def height(self):
138         """Height of the bitmap. (read only)"""
139         return self._height
140
141
142 class ColorConverter:
143     """Converts one color format to another. Color converter based on original displayio
144     code for consistency.
145     """
146
147     def __init__(self, *, dither=False):
148         """Create a ColorConverter object to convert color formats.
149         Only supports RGB888 to RGB565 currently.
150         :param bool dither: Adds random noise to dither the output image
151         """
152         self._dither = dither
153         self._depth = 16
154
155     def _compute_rgb565(self, color):
156         self._depth = 16
157         return (color >> 19) << 11 | ((color >> 10) & 0x3F) << 5 | (color >> 3) & 0x1F
158
159     def _compute_luma(self, color):
160         r8 = color >> 16
161         g8 = (color >> 8) & 0xFF
162         b8 = color & 0xFF
163         return (r8 * 19) / 255 + (g8 * 182) / 255 + (b8 + 54) / 255
164
165     def _compute_chroma(self, color):
166         r8 = color >> 16
167         g8 = (color >> 8) & 0xFF
168         b8 = color & 0xFF
169         return max(r8, g8, b8) - min(r8, g8, b8)
170
171     def _compute_hue(self, color):
172         r8 = color >> 16
173         g8 = (color >> 8) & 0xFF
174         b8 = color & 0xFF
175         max_color = max(r8, g8, b8)
176         chroma = self._compute_chroma(color)
177         if chroma == 0:
178             return 0
179         hue = 0
180         if max_color == r8:
181             hue = (((g8 - b8) * 40) / chroma) % 240
182         elif max_color == g8:
183             hue = (((b8 - r8) + (2 * chroma)) * 40) / chroma
184         elif max_color == b8:
185             hue = (((r8 - g8) + (4 * chroma)) * 40) / chroma
186         if hue < 0:
187             hue += 240
188
189         return hue
190
191     def _dither_noise_1(self, noise):
192         n = (n >> 13) ^ n
193         nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7FFFFFFF
194         return (nn / (1073741824.0 * 2)) * 255
195
196     def _dither_noise_2(self, x, y):
197         return self._dither_noise_1(x + y * 0xFFFF)
198
199     def _compute_tricolor(self):
200         pass
201
202     def convert(self, color):
203         "Converts the given RGB888 color to RGB565"
204         if self._dither:
205             return color  # To Do: return a dithered color
206         else:
207             return self._compute_rgb565(color)
208
209     def _pil_palette(self):
210         return None
211
212     @property
213     def dither(self):
214         "When true the color converter dithers the output by adding random noise when truncating to display bitdepth"
215         return self._dither
216
217     @dither.setter
218     def dither(self, value):
219         if not isinstance(value, bool):
220             raise ValueError("Value should be boolean")
221         self._dither = value
222
223
224 class Display:
225     """This initializes a display and connects it into CircuitPython. Unlike other objects in CircuitPython, Display objects live until ``displayio.release_displays()`` is called. This is done so that CircuitPython can use the display itself.
226
227     Most people should not use this class directly. Use a specific display driver instead that will contain the initialization sequence at minimum.
228     
229     .. class:: Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0, color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1, reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b, write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None, brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False, auto_refresh=True, native_frames_per_second=60)
230     
231     """
232
233     def __init__(
234         self,
235         display_bus,
236         init_sequence,
237         *,
238         width,
239         height,
240         colstart=0,
241         rowstart=0,
242         rotation=0,
243         color_depth=16,
244         grayscale=False,
245         pixels_in_byte_share_row=True,
246         bytes_per_cell=1,
247         reverse_pixels_in_byte=False,
248         set_column_command=0x2A,
249         set_row_command=0x2B,
250         write_ram_command=0x2C,
251         set_vertical_scroll=0,
252         backlight_pin=None,
253         brightness_command=None,
254         brightness=1.0,
255         auto_brightness=False,
256         single_byte_bounds=False,
257         data_as_commands=False,
258         auto_refresh=True,
259         native_frames_per_second=60
260     ):
261         """Create a Display object on the given display bus (`displayio.FourWire` or `displayio.ParallelBus`).
262
263         The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins with a command byte followed by a byte to determine the parameter count and if a delay is need after. When the top bit of the second byte is 1, the next byte will be the delay time in milliseconds. The remaining 7 bits are the parameter count excluding any delay byte. The third through final bytes are the remaining command parameters. The next byte will begin a new command definition. Here is a portion of ILI9341 init code:
264         .. code-block:: python
265         
266             init_sequence = (b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F" # Set Gamma
267                 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
268                 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
269             )
270             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
271         
272         The first command is 0xe1 with 15 (0xf) parameters following. The second and third are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters. Multiple byte literals (b”“) are merged together on load. The parens are needed to allow byte literals on subsequent lines.
273
274         The initialization sequence should always leave the display memory access inline with the scan of the display to minimize tearing artifacts.
275         """
276         self._bus = display_bus
277         self._set_column_command = set_column_command
278         self._set_row_command = set_row_command
279         self._write_ram_command = write_ram_command
280         self._brightness_command = brightness_command
281         self._data_as_commands = data_as_commands
282         self._single_byte_bounds = single_byte_bounds
283         self._width = width
284         self._height = height
285         self._colstart = colstart
286         self._rowstart = rowstart
287         self._rotation = rotation
288         self._auto_brightness = auto_brightness
289         self._brightness = brightness
290         self._auto_refresh = auto_refresh
291         self._initialize(init_sequence)
292         self._buffer = Image.new("RGB", (width, height))
293         self._subrectangles = []
294         self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
295         self._current_group = None
296         _displays.append(self)
297         self._refresh_thread = None
298         if self._auto_refresh:
299             self.auto_refresh = True
300
301     def _initialize(self, init_sequence):
302         i = 0
303         while i < len(init_sequence):
304             command = init_sequence[i]
305             data_size = init_sequence[i + 1]
306             delay = (data_size & 0x80) > 0
307             data_size &= ~0x80
308             data_byte = init_sequence[i + 2]
309             self._write(command, init_sequence[i + 2 : i + 2 + data_size])
310             delay_time_ms = 10
311             if delay:
312                 data_size += 1
313                 delay_time_ms = init_sequence[i + 1 + data_size]
314                 if delay_time_ms == 255:
315                     delay_time_ms = 500
316             time.sleep(delay_time_ms / 1000)
317             i += 2 + data_size
318
319     def _write(self, command, data):
320         if self._single_byte_bounds:
321             self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
322         else:
323             self._bus.send(True, bytes([command]), toggle_every_byte=True)
324             self._bus.send(False, data)
325
326     def _release(self):
327         self._bus.release()
328         self._bus = None
329
330     def show(self, group):
331         """Switches to displaying the given group of layers. When group is None, the default CircuitPython terminal will be shown.
332         """
333         self._current_group = group
334
335     def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
336         """When auto refresh is off, waits for the target frame rate and then refreshes the display, returning True. If the call has taken too long since the last refresh call for the given target frame rate, then the refresh returns False immediately without updating the screen to hopefully help getting caught up.
337
338         If the time since the last successful refresh is below the minimum frame rate, then an exception will be raised. Set minimum_frames_per_second to 0 to disable.
339
340         When auto refresh is on, updates the display immediately. (The display will also update without calls to this.)
341         """
342         # Go through groups and and add each to buffer
343         if self._current_group is not None:
344             buffer = Image.new("RGB", (self._width, self._height))
345             # Recursively have everything draw to the image
346             self._current_group._fill_area(buffer)
347             # save image to buffer (or probably refresh buffer so we can compare)
348             self._buffer.paste(buffer)
349         print("refreshing")
350         time.sleep(1)
351         # Eventually calculate dirty rectangles here
352         self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
353         
354         for area in self._subrectangles:
355             self._refresh_display_area(area)
356
357     def _refresh_loop(self):
358         while self._auto_refresh:
359             self.refresh()
360
361     def _refresh_display_area(self, rectangle):
362         """Loop through dirty rectangles and redraw that area."""
363         """Read or write a block of data."""
364         data = numpy.array(self._buffer.crop(rectangle).convert("RGB")).astype("uint16")
365         color = (
366             ((data[:, :, 0] & 0xF8) << 8)
367             | ((data[:, :, 1] & 0xFC) << 3)
368             | (data[:, :, 2] >> 3)
369         )
370         
371         pixels = list(numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist())
372         
373         self._write(
374             self._set_column_command,
375             self._encode_pos(rectangle.x1 + self._colstart, rectangle.x2 + self._colstart)
376         )
377         self._write(
378             self._set_row_command,
379             self._encode_pos(rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart)
380         )
381         self._write(self._write_ram_command, pixels)
382
383     def _encode_pos(self, x, y):
384         """Encode a postion into bytes."""
385         return struct.pack(self._bounds_encoding, x, y)
386
387     def fill_row(self, y, buffer):
388         pass
389
390     @property
391     def auto_refresh(self):
392         return self._auto_refresh
393
394     @auto_refresh.setter
395     def auto_refresh(self, value):
396         self._auto_refresh = value
397         if self._refresh_thread is None:
398             self._refresh_thread = threading.Thread(target=self._refresh_loop, daemon=True)
399         if value and not self._refresh_thread.is_alive():
400             # Start the thread
401             self._refresh_thread.start()
402         elif not value and self._refresh_thread.is_alive():
403             # Stop the thread
404             self._refresh_thread.join()
405
406     @property
407     def brightness(self):
408         """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`. When `auto_brightness` is True, the value of `brightness` will change automatically. If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
409         """
410         return self._brightness
411
412     @brightness.setter
413     def brightness(self, value):
414         self._brightness = value
415
416     @property
417     def auto_brightness(self):
418         """True when the display brightness is adjusted automatically, based on an ambient light sensor or other method. Note that some displays may have this set to True by default, but not actually implement automatic brightness adjustment. `auto_brightness` is set to False if `brightness` is set manually.
419         """
420         return self._auto_brightness
421
422     @auto_brightness.setter
423     def auto_brightness(self, value):
424         self._auto_brightness = value
425
426     @property
427     def width(self):
428         return self._width
429
430     @property
431     def height(self):
432         return self._height
433
434     @property
435     def rotation(self):
436         """The rotation of the display as an int in degrees."""
437         return self._rotation
438
439     @rotation.setter
440     def rotation(self, value):
441         if value not in (0, 90, 180, 270):
442             raise ValueError("Rotation must be 0/90/180/270")
443         self._rotation = value
444
445     @property
446     def bus(self):
447         return self._bus
448
449
450 class EPaperDisplay:
451     def __init__(
452         self,
453         display_bus,
454         start_sequence,
455         stop_sequence,
456         *,
457         width,
458         height,
459         ram_width,
460         ram_height,
461         colstart=0,
462         rowstart=0,
463         rotation=0,
464         set_column_window_command=None,
465         set_row_window_command=None,
466         single_byte_bounds=False,
467         write_black_ram_command,
468         black_bits_inverted=False,
469         write_color_ram_command=None,
470         color_bits_inverted=False,
471         highlight_color=0x000000,
472         refresh_display_command,
473         refresh_time=40,
474         busy_pin=None,
475         busy_state=True,
476         seconds_per_frame=180,
477         always_toggle_chip_select=False
478     ):
479         """
480         Create a EPaperDisplay object on the given display bus (displayio.FourWire or displayio.ParallelBus).
481
482         The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every command begins with a command byte followed by a byte to determine the parameter count and if a delay is need after. When the top bit of the second byte is 1, the next byte will be the delay time in milliseconds. The remaining 7 bits are the parameter count excluding any delay byte. The third through final bytes are the remaining command parameters. The next byte will begin a new command definition.
483         """
484         pass
485
486     def show(self, group):
487         """Switches to displaying the given group of layers. When group is None, the default CircuitPython terminal will be shown.
488         """
489         pass
490
491     def refresh(self):
492         """Refreshes the display immediately or raises an exception if too soon. Use ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
493         """
494         pass
495
496     @property
497     def time_to_refresh(self):
498         """Time, in fractional seconds, until the ePaper display can be refreshed."""
499         return 0
500
501     @property
502     def width(self):
503         pass
504
505     @property
506     def height(self):
507         pass
508
509     @property
510     def bus(self):
511         pass
512
513
514 class FourWire:
515     """Manage updating a display over SPI four wire protocol in the background while
516     Python code runs. It doesn’t handle display initialization.
517     """
518
519     def __init__(
520         self,
521         spi_bus,
522         *,
523         command,
524         chip_select,
525         reset=None,
526         baudrate=24000000,
527         polarity=0,
528         phase=0
529     ):
530         """Create a FourWire object associated with the given pins.
531
532         The SPI bus and pins are then in use by the display until displayio.release_displays() is called even after a reload. (It does this so CircuitPython can use the display after your code is done.) So, the first time you initialize a display bus in code.py you should call :py:func`displayio.release_displays` first, otherwise it will error after the first code.py run.
533         """
534         self._dc = digitalio.DigitalInOut(command)
535         self._dc.switch_to_output()
536         self._chip_select = digitalio.DigitalInOut(chip_select)
537         self._chip_select.switch_to_output(value=True)
538
539         if reset is not None:
540             self._reset = digitalio.DigitalInOut(reset)
541             self._reset.switch_to_output(value=True)
542         else:
543             self._reset = None
544         self._spi = spi_bus
545         while self._spi.try_lock():
546             pass
547         self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
548         self._spi.unlock()
549
550     def _release(self):
551         self.reset()
552         self._spi.deinit()
553         self._dc.deinit()
554         self._chip_select.deinit()
555         if self._reset is not None:
556             self._reset.deinit()
557
558     def reset(self):
559         if self._reset is not None:
560             self._reset.value = False
561             time.sleep(0.001)
562             self._reset.value = True
563             time.sleep(0.001)
564
565     def send(self, is_command, data, *, toggle_every_byte=False):
566         while self._spi.try_lock():
567             pass
568         self._dc.value = not is_command
569         if toggle_every_byte:
570             for byte in data:
571                 self._spi.write(bytes([byte]))
572                 self._chip_select.value = True
573                 time.sleep(0.000001)
574                 self._chip_select.value = False
575         else:
576             self._spi.write(data)
577         self._spi.unlock()
578
579
580 class Group:
581     """Manage a group of sprites and groups and how they are inter-related."""
582
583     def __init__(self, *, max_size=4, scale=1, x=0, y=0):
584         """Create a Group of a given size and scale. Scale is in
585         one dimension. For example, scale=2 leads to a layer’s
586         pixel being 2x2 pixels when in the group.
587         """
588         if not isinstance(max_size, int) or max_size < 1:
589             raise ValueError("Max Size must be an integer and >= 1")
590         self._max_size = max_size
591         if not isinstance(scale, int) or scale < 1:
592             raise ValueError("Scale must be an integer and >= 1")
593         self._scale = scale
594         self._x = x
595         self._y = y
596         self._hidden = False
597         self._layers = []
598         self._supported_types = (TileGrid, Group)
599         print("Creating Group")
600
601     def append(self, layer):
602         """Append a layer to the group. It will be drawn
603         above other layers.
604         """
605         if not isinstance(layer, self._supported_types):
606             raise ValueError("Invalid Group Memeber")
607         if len(self._layers) == self._max_size:
608             raise RuntimeError("Group full")
609         self._layers.append(layer)
610
611     def insert(self, index, layer):
612         """Insert a layer into the group."""
613         if not isinstance(layer, self._supported_types):
614             raise ValueError("Invalid Group Memeber")
615         if len(self._layers) == self._max_size:
616             raise RuntimeError("Group full")
617         self._layers.insert(index, layer)
618
619     def index(self, layer):
620         """Returns the index of the first copy of layer.
621         Raises ValueError if not found.
622         """
623         pass
624
625     def pop(self, index=-1):
626         """Remove the ith item and return it."""
627         return self._layers.pop(index)
628
629     def remove(self, layer):
630         """Remove the first copy of layer. Raises ValueError
631         if it is not present."""
632         pass
633
634     def __len__(self):
635         """Returns the number of layers in a Group"""
636         return len(self._layers)
637
638     def __getitem__(self, index):
639         """Returns the value at the given index."""
640         return self._layers[index]
641
642     def __setitem__(self, index, value):
643         """Sets the value at the given index."""
644         self._layers[index] = value
645
646     def __delitem__(self, index):
647         """Deletes the value at the given index."""
648         del self._layers[index]
649
650     def _fill_area(self, buffer):
651         if self._hidden:
652             return
653
654         for layer in self._layers:
655             if isinstance(layer, (Group, TileGrid)):
656                 layer._fill_area(buffer)
657
658     @property
659     def hidden(self):
660         return self._hidden
661
662     @hidden.setter
663     def hidden(self, value):
664         if not isinstance(value, (bool, int)):
665             raise ValueError("Expecting a boolean or integer value")
666         self._hidden = bool(value)
667
668     @property
669     def scale(self):
670         return self._scale
671
672     @scale.setter
673     def scale(self, value):
674         if not isinstance(value, int) or value < 1:
675             raise ValueError("Scale must be an integer and at least 1")
676         self._scale = value
677
678     @property
679     def x(self):
680         return self._x
681
682     @x.setter
683     def x(self, value):
684         if not isinstance(value, int):
685             raise ValueError("x must be an integer")
686         self._x = value
687
688     @property
689     def y(self):
690         return self._y
691
692     @y.setter
693     def y(self, value):
694         if not isinstance(value, int):
695             raise ValueError("y must be an integer")
696         self._y = value
697
698
699 class I2CDisplay:
700     """Manage updating a display over I2C in the background while Python code runs. It doesn’t handle display initialization.
701     """
702
703     def __init__(self, i2c_bus, *, device_address, reset=None):
704         """Create a I2CDisplay object associated with the given I2C bus and reset pin.
705
706         The I2C bus and pins are then in use by the display until displayio.release_displays() is called even after a reload. (It does this so CircuitPython can use the display after your code is done.) So, the first time you initialize a display bus in code.py you should call :py:func`displayio.release_displays` first, otherwise it will error after the first code.py run.
707         """
708         pass
709
710     def reset(self):
711         pass
712
713     def send(self, command, data):
714         pass
715
716
717 class OnDiskBitmap:
718     """
719     Loads values straight from disk. This minimizes memory use but can lead to much slower pixel load times.
720     These load times may result in frame tearing where only part of the image is visible."""
721
722     def __init__(self, file):
723         self._image = Image.open(file)
724
725     @property
726     def width(self):
727         """Width of the bitmap. (read only)"""
728         return self._image.width
729
730     @property
731     def height(self):
732         """Height of the bitmap. (read only)"""
733         return self._image.height
734
735 class Palette:
736     """Map a pixel palette_index to a full color. Colors are transformed to the display’s format internally to save memory."""
737
738     def __init__(self, color_count):
739         """Create a Palette object to store a set number of colors."""
740         self._needs_refresh = False
741         
742         self._colors = []
743         for _ in range(color_count):
744             self._colors.append(self._make_color(0))
745
746     def _make_color(self, value):
747         color = {
748             "transparent": False,
749             "rgb888": 0,
750         }
751         color_converter = ColorConverter()
752         if isinstance(value, (tuple, list, bytes, bytearray)):
753             value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
754         elif isinstance(value, int):
755             if not 0 <= value <= 0xFFFFFF:
756                 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
757         else:
758             raise TypeError("Color buffer must be a buffer, tuple, list, or int")
759         color["rgb888"] = value
760         self._needs_refresh = True
761
762         return color
763
764     def __len__(self):
765         """Returns the number of colors in a Palette"""
766         return len(self._colors)
767
768     def __setitem__(self, index, value):
769         """Sets the pixel color at the given index. The index should be an integer in the range 0 to color_count-1.
770
771         The value argument represents a color, and can be from 0x000000 to 0xFFFFFF (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or 4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
772         """
773         if self._colors[index]["rgb888"] != value:
774             self._colors[index] = self._make_color(value)
775
776     def __getitem__(self, index):
777         if not 0 <= index < len(self._colors):
778             raise ValueError("Palette index out of range")
779         return self._colors[index]
780
781     def make_transparent(self, palette_index):
782         self._colors[palette_index].transparent = True
783
784     def make_opaque(self, palette_index):
785         self._colors[palette_index].transparent = False
786
787 class ParallelBus:
788     """Manage updating a display over 8-bit parallel bus in the background while Python code runs.
789     This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
790     It doesn’t handle display initialization.
791     """
792
793     def __init__(self, i2c_bus, *, device_address, reset=None):
794         """Create a ParallelBus object associated with the given pins. The bus is inferred from data0 by implying the next 7 additional pins on a given GPIO port.
795
796         The parallel bus and pins are then in use by the display until displayio.release_displays() is called even after a reload. (It does this so CircuitPython can use the display after your code is done.) So, the first time you initialize a display bus in code.py you should call :py:func`displayio.release_displays` first, otherwise it will error after the first code.py run.
797         """
798         pass
799
800     def reset(self):
801         """Performs a hardware reset via the reset pin. Raises an exception if called when no reset pin is available.
802         """
803         pass
804
805     def send(self, command, data):
806         """Sends the given command value followed by the full set of data. Display state, such as
807         vertical scroll, set via ``send`` may or may not be reset once the code is done.
808         """
809         pass
810
811
812 class Shape(Bitmap):
813     """Create a Shape object with the given fixed size. Each pixel is one bit and is stored by the column
814     boundaries of the shape on each row. Each row’s boundary defaults to the full row.
815     """
816
817     def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
818         """Create a Shape object with the given fixed size. Each pixel is one bit and is stored by the
819         column boundaries of the shape on each row. Each row’s boundary defaults to the full row.
820         """
821         pass
822
823     def set_boundary(self, y, start_x, end_x):
824         """Loads pre-packed data into the given row."""
825         pass
826
827
828 class TileGrid:
829     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple grids can share bitmaps and pixel shaders.
830
831     A single tile grid is also known as a Sprite.
832     """
833
834     def __init__(
835         self,
836         bitmap,
837         *,
838         pixel_shader,
839         width=1,
840         height=1,
841         tile_width=None,
842         tile_height=None,
843         default_tile=0,
844         x=0,
845         y=0
846     ):
847         """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is used to convert the value and its location to a display native pixel color. This may be a simple color palette lookup, a gradient, a pattern or a color transformer.
848
849         tile_width and tile_height match the height of the bitmap by default.
850         """
851         if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
852             raise ValueError("Unsupported Bitmap type")
853         self._bitmap = bitmap
854         bitmap_width = bitmap.width
855         bitmap_height = bitmap.height
856         
857         if not isinstance(pixel_shader, (ColorConverter, Palette)):
858             raise ValueError("Unsupported Pixel Shader type")
859         self._pixel_shader = pixel_shader
860         self._hidden = False
861         self._x = x
862         self._y = y
863         self._width = width # Number of Tiles Wide
864         self._height = height # Number of Tiles High
865         if tile_width is None:
866             tile_width = bitmap_width
867         if tile_height is None:
868             tile_height = bitmap_height
869         if bitmap_width % tile_width != 0:
870             raise ValueError("Tile width must exactly divide bitmap width")
871         self._tile_width = tile_width
872         if bitmap_height % tile_height != 0:
873             raise ValueError("Tile height must exactly divide bitmap height")
874         self._tile_height = tile_height
875         self._tiles = (self._width * self._height) * [default_tile]
876
877     def _fill_area(self, buffer):
878         """Draw onto the image"""
879         print("Drawing TileGrid")
880         if self._hidden:
881             return
882         
883         image = Image.new("RGB", (self._width * self._tile_width, self._height * self._tile_height))
884         
885         for tile_x in range(self._width):
886             for tile_y in range(self._height):
887                 tile_index = self._tiles[tile_y * self._width + tile_x]
888                 tile_index_x = tile_index % self._width
889                 tile_index_y = tile_index // self._width
890                 for pixel_x in range(self._tile_width):
891                     for pixel_y in range(self._tile_height):
892                         image_x = tile_x * self._tile_width + pixel_x
893                         image_y = tile_y * self._tile_height + pixel_y
894                         bitmap_x = tile_index_x * self._tile_width + pixel_x
895                         bitmap_y = tile_index_y * self._tile_height + pixel_y
896                         pixel_color = self._pixel_shader[self._bitmap[bitmap_x, bitmap_y]]
897                         image.putpixel((image_x, image_y), pixel_color["rgb888"])
898
899         buffer.paste(image, (self._x, self._y))
900         """
901         Strategy
902         ------------
903         Draw on it
904         Apply the palette
905         Do any transforms or mirrors or whatever
906         Paste into buffer at our x,y position
907         """
908         
909
910     @property
911     def hidden(self):
912         """True when the TileGrid is hidden. This may be False even when a part of a hidden Group."""
913         return self._hidden
914
915     @hidden.setter
916     def hidden(self, value):
917         if not isinstance(value, (bool, int)):
918             raise ValueError("Expecting a boolean or integer value")
919         self._hidden = bool(value)
920
921     @property
922     def x(self):
923         """X position of the left edge in the parent."""
924         return self._x
925
926     @property
927     def y(self):
928         """Y position of the top edge in the parent."""
929         return self._y
930
931     @property
932     def flip_x(self):
933         """If true, the left edge rendered will be the right edge of the right-most tile."""
934         return self._flip_x
935
936     @flip_x.setter
937     def flip_x(self, value):
938         if not isinstance(value, bool):
939             raise TypeError("Flip X should be a boolean type")
940         self._flip_x = value
941
942     @property
943     def flip_y(self):
944         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
945         return self._flip_y
946
947     @flip_y.setter
948     def flip_y(self, value):
949         if not isinstance(value, bool):
950             raise TypeError("Flip Y should be a boolean type")
951         self._flip_y = value
952
953     @property
954     def transpose_xy(self):
955         """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90 degree
956         rotation can be achieved along with the corresponding mirrored version.
957         """
958         return self._transpose_xy
959
960     @transpose_xy.setter
961     def transpose_xy(self, value):
962         if not isinstance(value, bool):
963             raise TypeError("Transpose XY should be a boolean type")
964         self._transpose_xy = value
965
966     @property
967     def pixel_shader(self):
968         """The pixel shader of the tilegrid."""
969         pass
970
971     def __getitem__(self, index):
972         """Returns the tile index at the given index. The index can either be
973         an x,y tuple or an int equal to ``y * width + x``'.
974         """
975         if isinstance(index, (tuple, list)):
976             x = index[0]
977             y = index[1]
978             index = y * self._width + x
979         elif ininstance(index, int):
980             x = index % self._width
981             y = index // self._width
982         if x > self._width or y > self._height:
983             raise ValueError("Tile index out of bounds")
984         return self._tiles[index]
985
986     def __setitem__(self, index, value):
987         """Sets the tile index at the given index. The index can either be
988         an x,y tuple or an int equal to ``y * width + x``.
989         """
990         if isinstance(index, (tuple, list)):
991             x = index[0]
992             y = index[1]
993             index = y * self._width + x
994         elif ininstance(index, int):
995             x = index % self._width
996             y = index // self._width
997         if x > width or y > self._height or index > len(self._tiles):
998             raise ValueError("Tile index out of bounds")
999         if not 0 <= value <= 255:
1000             raise ValueError("Tile value out of bounds")
1001         self._tiles[index] = value