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