Created
October 13, 2025 13:54
-
-
Save DonizeteVida/83e628e6cad177a10e032217642da05b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import serial | |
| import PIL | |
| import PIL.ImageFile | |
| from typing import Iterator, Literal, Tuple | |
| from enum import IntEnum | |
| import numpy as np | |
| from PIL import Image, GifImagePlugin, ImageChops, ImageSequence | |
| class Command(IntEnum): | |
| RESET = 101 # Resets the display | |
| CLEAR = 102 # Clears the display to a white screen | |
| TO_BLACK = 103 # Makes the screen go black. NOT TESTED | |
| SCREEN_OFF = 108 # Turns the screen off | |
| SCREEN_ON = 109 # Turns the screen on | |
| SET_BRIGHTNESS = 110 # Sets the screen brightness | |
| SET_ORIENTATION = 121 # Sets the screen orientation | |
| DISPLAY_BITMAP = 197 # Displays an image on the screen | |
| # Commands below are only supported by next generation Turing Smart screens | |
| LCD_28 = 40 # ? | |
| LCD_29 = 41 # ? | |
| HELLO = 69 # Asks the screen for its model: 3.5", 5" or 7" | |
| SET_MIRROR = 122 # Mirrors the rendering on the screen | |
| DISPLAY_PIXELS = 195 # Displays a list of pixels than can be non-contiguous in one command, useful for line charts | |
| class Orientation(IntEnum): | |
| PORTRAIT = 0 | |
| LANDSCAPE = 2 | |
| REVERSE_PORTRAIT = 1 | |
| REVERSE_LANDSCAPE = 3 | |
| _serial = serial.Serial("COM3", 115200, timeout=1, rtscts=True) | |
| width = 320 | |
| height = 480 | |
| def WriteLine(line: bytes): | |
| _serial.write(line) | |
| def WriteData(byteBuffer: bytearray): | |
| WriteLine(bytes(byteBuffer)) | |
| def SendCommand(cmd: Command, x: int, y: int, ex: int, ey: int): | |
| byteBuffer = bytearray(6) | |
| byteBuffer[0] = (x >> 2) | |
| byteBuffer[1] = (((x & 3) << 6) + (y >> 4)) | |
| byteBuffer[2] = (((y & 15) << 4) + (ex >> 6)) | |
| byteBuffer[3] = (((ex & 63) << 2) + (ey >> 8)) | |
| byteBuffer[4] = (ey & 255) | |
| byteBuffer[5] = cmd | |
| WriteData(byteBuffer) | |
| def image_to_RGB565(image: Image.Image, endianness: Literal["big", "little"] = "little") -> bytes: | |
| if image.mode not in ["RGB", "RGBA"]: | |
| # we need the first 3 channels to be R, G and B | |
| image = image.convert("RGB") | |
| rgb = np.asarray(image) | |
| # flatten the first 2 dimensions (width and height) into a single stream | |
| # of RGB pixels | |
| rgb = rgb.reshape((image.size[1] * image.size[0], -1)) | |
| # extract R, G, B channels and promote them to 16 bits | |
| r = rgb[:, 0].astype(np.uint16) | |
| g = rgb[:, 1].astype(np.uint16) | |
| b = rgb[:, 2].astype(np.uint16) | |
| # construct RGB565 | |
| r = r >> 3 | |
| g = g >> 2 | |
| b = b >> 3 | |
| rgb565 = (r << 11) | (g << 5) | b | |
| # serialize to the correct endianness | |
| if endianness == "big": | |
| typ = ">u2" | |
| else: | |
| typ = "<u2" | |
| return rgb565.astype(typ).tobytes() | |
| def chunked(data: bytes, chunk_size: int) -> Iterator[bytes]: | |
| for i in range(0, len(data), chunk_size): | |
| yield data[i: i + chunk_size] | |
| def write_image(chunk: bytes): | |
| for b in chunked(chunk, width * 8): | |
| WriteLine(b) | |
| def SetOrientation(orientation: Orientation = Orientation.PORTRAIT): | |
| x = 0 | |
| y = 0 | |
| ex = 0 | |
| ey = 0 | |
| byteBuffer = bytearray(16) | |
| byteBuffer[0] = (x >> 2) | |
| byteBuffer[1] = (((x & 3) << 6) + (y >> 4)) | |
| byteBuffer[2] = (((y & 15) << 4) + (ex >> 6)) | |
| byteBuffer[3] = (((ex & 63) << 2) + (ey >> 8)) | |
| byteBuffer[4] = (ey & 255) | |
| byteBuffer[5] = Command.SET_ORIENTATION | |
| byteBuffer[6] = (orientation + 100) | |
| byteBuffer[7] = (width >> 8) | |
| byteBuffer[8] = (width & 255) | |
| byteBuffer[9] = (height >> 8) | |
| byteBuffer[10] = (height & 255) | |
| _serial.write(bytes(byteBuffer)) | |
| def Clear(): | |
| SetOrientation(Orientation.PORTRAIT) # Bug: orientation needs to be PORTRAIT before clearing | |
| SendCommand(Command.CLEAR, 0, 0, 0, 0) | |
| def ScreenOff(): | |
| SendCommand(Command.SCREEN_OFF, 0, 0, 0, 0) | |
| def ScreenOn(): | |
| SendCommand(Command.SCREEN_ON, 0, 0, 0, 0) | |
| ScreenOff() | |
| ScreenOn() | |
| image = Image.open("matrix.gif") | |
| diffs_debug = list[PIL.ImageFile.ImageFile]() | |
| boxes_debug = list[PIL.ImageFile.ImageFile]() | |
| diff_crops = list[ | |
| Tuple[ | |
| PIL.ImageFile.ImageFile, | |
| Tuple[int, int, int, int] | |
| ] | |
| ]() | |
| if isinstance(image, GifImagePlugin.GifImageFile): | |
| frames = [frame.copy().convert("RGB") for i, frame in enumerate(ImageSequence.Iterator(image))] | |
| prev = frames[0] | |
| for i in range(len(frames)): | |
| current = frames[i] | |
| diff = ImageChops.difference(prev, current) | |
| diffs_debug.append(diff) | |
| diff_box = diff.getbbox() if diff.getbbox() else current.getbbox() | |
| diff_crop = current.crop(diff_box) | |
| boxes_debug.append(diff_crop) | |
| diff_crops.append((diff_crop, diff_box)) | |
| prev = current | |
| diffs_debug[0].save("_diffs.gif", append_images=diffs_debug[1:]) | |
| boxes_debug[0].save("_boxes.gif", append_images=boxes_debug[1:]) | |
| for (diff_crop, diff_box) in diff_crops: | |
| left, upper, right, lower = diff_box | |
| SendCommand(Command.DISPLAY_BITMAP, left, upper, right - 1, lower - 1) | |
| write_image(image_to_RGB565(diff_crop)) | |
| else: | |
| SendCommand(Command.DISPLAY_BITMAP, 0, 0, width - 1, height - 1) | |
| write_image(image_to_RGB565(image)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment