Skip to content

Instantly share code, notes, and snippets.

@DonizeteVida
Created October 13, 2025 13:54
Show Gist options
  • Select an option

  • Save DonizeteVida/83e628e6cad177a10e032217642da05b to your computer and use it in GitHub Desktop.

Select an option

Save DonizeteVida/83e628e6cad177a10e032217642da05b to your computer and use it in GitHub Desktop.
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