|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# A simple Python manager for "Turing Smart Screen" 3.5" IPS USB-C display |
| 3 | +# https://github.com/mathoudebine/turing-smart-screen-python |
| 4 | + |
| 5 | +import os |
| 6 | +import signal |
| 7 | +import struct |
| 8 | +from datetime import datetime |
| 9 | +from time import sleep |
| 10 | + |
| 11 | +import serial # Install pyserial : pip install pyserial |
| 12 | +from PIL import Image, ImageDraw, ImageFont # Install PIL or Pillow |
| 13 | + |
| 14 | +# Set your COM port e.g. COM3 for Windows, /dev/ttyACM0 for Linux... |
| 15 | +COM_PORT = "/dev/ttyACM0" |
| 16 | +# COM_PORT = "COM5" |
| 17 | + |
| 18 | +DISPLAY_WIDTH = 320 |
| 19 | +DISPLAY_HEIGHT = 480 |
| 20 | + |
| 21 | + |
| 22 | +class Command: |
| 23 | + RESET = 101 |
| 24 | + CLEAR = 102 |
| 25 | + SCREEN_OFF = 108 |
| 26 | + SCREEN_ON = 109 |
| 27 | + SET_BRIGHTNESS = 110 |
| 28 | + DISPLAY_BITMAP = 197 |
| 29 | + |
| 30 | + |
| 31 | +def SendReg(ser: serial.Serial, cmd: int, x: int, y: int, ex: int, ey: int): |
| 32 | + byteBuffer = bytearray(6) |
| 33 | + byteBuffer[0] = (x >> 2) |
| 34 | + byteBuffer[1] = (((x & 3) << 6) + (y >> 4)) |
| 35 | + byteBuffer[2] = (((y & 15) << 4) + (ex >> 6)) |
| 36 | + byteBuffer[3] = (((ex & 63) << 2) + (ey >> 8)) |
| 37 | + byteBuffer[4] = (ey & 255) |
| 38 | + byteBuffer[5] = cmd |
| 39 | + ser.write(bytes(byteBuffer)) |
| 40 | + |
| 41 | + |
| 42 | +def Reset(ser: serial.Serial): |
| 43 | + SendReg(ser, Command.RESET, 0, 0, 0, 0) |
| 44 | + |
| 45 | + |
| 46 | +def Clear(ser: serial.Serial): |
| 47 | + SendReg(ser, Command.CLEAR, 0, 0, 0, 0) |
| 48 | + |
| 49 | + |
| 50 | +def ScreenOff(ser: serial.Serial): |
| 51 | + SendReg(ser, Command.SCREEN_OFF, 0, 0, 0, 0) |
| 52 | + |
| 53 | + |
| 54 | +def ScreenOn(ser: serial.Serial): |
| 55 | + SendReg(ser, Command.SCREEN_ON, 0, 0, 0, 0) |
| 56 | + |
| 57 | + |
| 58 | +def SetBrightness(ser: serial.Serial, level: int): |
| 59 | + # Level : 0 (brightest) - 255 (darkest) |
| 60 | + assert 255 >= level >= 0, 'Brightness level must be [0-255]' |
| 61 | + SendReg(ser, Command.SET_BRIGHTNESS, level, 0, 0, 0) |
| 62 | + |
| 63 | + |
| 64 | +def DisplayPILImage(ser: serial.Serial, image: Image, x: int, y: int): |
| 65 | + image_height = image.size[1] |
| 66 | + image_width = image.size[0] |
| 67 | + |
| 68 | + assert image_height > 0, 'Image width must be > 0' |
| 69 | + assert image_width > 0, 'Image height must be > 0' |
| 70 | + |
| 71 | + SendReg(ser, Command.DISPLAY_BITMAP, x, y, x + image_width - 1, y + image_height - 1) |
| 72 | + |
| 73 | + pix = image.load() |
| 74 | + line = bytes() |
| 75 | + for h in range(image_height): |
| 76 | + for w in range(image_width): |
| 77 | + R = pix[w, h][0] >> 3 |
| 78 | + G = pix[w, h][1] >> 2 |
| 79 | + B = pix[w, h][2] >> 3 |
| 80 | + |
| 81 | + rgb = (R << 11) | (G << 5) | B |
| 82 | + line += struct.pack('H', rgb) |
| 83 | + |
| 84 | + # Send image data by multiple of DISPLAY_WIDTH bytes |
| 85 | + if len(line) >= DISPLAY_WIDTH * 8: |
| 86 | + ser.write(line) |
| 87 | + line = bytes() |
| 88 | + |
| 89 | + # Write last line if needed |
| 90 | + if len(line) > 0: |
| 91 | + ser.write(line) |
| 92 | + |
| 93 | + sleep(0.01) # Wait 10 ms after picture display |
| 94 | + |
| 95 | + |
| 96 | +def DisplayBitmap(ser: serial.Serial, bitmap_path: str, x=0, y=0): |
| 97 | + image = Image.open(bitmap_path) |
| 98 | + DisplayPILImage(ser, image, x, y) |
| 99 | + |
| 100 | + |
| 101 | +def DisplayText(ser: serial.Serial, text: str, x=0, y=0, |
| 102 | + font="roboto/Roboto-Regular.ttf", |
| 103 | + font_size=20, |
| 104 | + font_color=(0, 0, 0), |
| 105 | + background_color=(255, 255, 255), |
| 106 | + background_image: str = None): |
| 107 | + # Convert text to bitmap using PIL and display it |
| 108 | + # Provide the background image path to display text with transparent background |
| 109 | + |
| 110 | + assert len(text) > 0, 'Text must not be empty' |
| 111 | + assert font_size > 0, "Font size must be > 0" |
| 112 | + |
| 113 | + if background_image is None: |
| 114 | + # A text bitmap is created with max width/height by default : text with solid background |
| 115 | + text_image = Image.new('RGB', (DISPLAY_WIDTH, DISPLAY_HEIGHT), background_color) |
| 116 | + else: |
| 117 | + # The text bitmap is created from provided background image : text with transparent background |
| 118 | + text_image = Image.open(background_image) |
| 119 | + |
| 120 | + # Draw text with specified color & font (also crop if text overflows display) |
| 121 | + font = ImageFont.truetype("./res/fonts/" + font, font_size) |
| 122 | + d = ImageDraw.Draw(text_image) |
| 123 | + d.text((x, y), text, font=font, fill=font_color) |
| 124 | + |
| 125 | + # Crop text bitmap to keep only the text |
| 126 | + left, top, text_width, text_height = d.textbbox((0,0), text, font=font) |
| 127 | + text_image = text_image.crop(box=(x, y, min(x + text_width, DISPLAY_WIDTH), min(y + text_height, DISPLAY_HEIGHT))) |
| 128 | + |
| 129 | + DisplayPILImage(ser, text_image, x, y) |
| 130 | + |
| 131 | + |
| 132 | +def DisplayProgressBar(ser: serial.Serial, x: int, y: int, width: int, height: int, min_value=0, max_value=100, |
| 133 | + value=50, |
| 134 | + bar_color=(0, 0, 0), |
| 135 | + bar_outline=True, |
| 136 | + background_color=(255, 255, 255), |
| 137 | + background_image: str = None): |
| 138 | + # Generate a progress bar and display it |
| 139 | + # Provide the background image path to display progress bar with transparent background |
| 140 | + |
| 141 | + assert x + width <= DISPLAY_WIDTH, 'Progress bar width exceeds display width' |
| 142 | + assert y + height <= DISPLAY_HEIGHT, 'Progress bar height exceeds display height' |
| 143 | + assert min_value <= value <= max_value, 'Progress bar value shall be between min and max' |
| 144 | + |
| 145 | + if background_image is None: |
| 146 | + # A bitmap is created with solid background |
| 147 | + bar_image = Image.new('RGB', (width, height), background_color) |
| 148 | + else: |
| 149 | + # A bitmap is created from provided background image |
| 150 | + bar_image = Image.open(background_image) |
| 151 | + |
| 152 | + # Crop bitmap to keep only the progress bar background |
| 153 | + bar_image = bar_image.crop(box=(x, y, x + width, y + height)) |
| 154 | + |
| 155 | + # Draw progress bar |
| 156 | + bar_filled_width = value / (max_value - min_value) * width |
| 157 | + draw = ImageDraw.Draw(bar_image) |
| 158 | + draw.rectangle([0, 0, bar_filled_width-1, height-1], fill=bar_color, outline=bar_color) |
| 159 | + |
| 160 | + if bar_outline: |
| 161 | + # Draw outline |
| 162 | + draw.rectangle([0, 0, width-1, height-1], fill=None, outline=bar_color) |
| 163 | + |
| 164 | + DisplayPILImage(ser, bar_image, x, y) |
| 165 | + |
| 166 | + |
| 167 | +stop = False |
| 168 | + |
| 169 | +if __name__ == "__main__": |
| 170 | + |
| 171 | + def sighandler(signum, frame): |
| 172 | + global stop |
| 173 | + stop = True |
| 174 | + |
| 175 | + |
| 176 | + # Set the signal handlers, to send a complete frame to the LCD before exit |
| 177 | + signal.signal(signal.SIGINT, sighandler) |
| 178 | + signal.signal(signal.SIGTERM, sighandler) |
| 179 | + is_posix = os.name == 'posix' |
| 180 | + if is_posix: |
| 181 | + signal.signal(signal.SIGQUIT, sighandler) |
| 182 | + |
| 183 | + # Do not change COM port settings unless you know what you are doing |
| 184 | + lcd_comm = serial.Serial(COM_PORT, 115200, timeout=1, rtscts=1) |
| 185 | + |
| 186 | + # Clear screen (blank) |
| 187 | + Clear(lcd_comm) |
| 188 | + |
| 189 | + # Set brightness to max value |
| 190 | + SetBrightness(lcd_comm, 0) |
| 191 | + |
| 192 | + # Display sample picture |
| 193 | + DisplayBitmap(lcd_comm, "res/example.png") |
| 194 | + |
| 195 | + # Display sample text |
| 196 | + DisplayText(lcd_comm, "Basic text", 50, 100) |
| 197 | + |
| 198 | + # Display custom text with solid background |
| 199 | + DisplayText(lcd_comm, "Custom italic text", 5, 150, |
| 200 | + font="roboto/Roboto-Italic.ttf", |
| 201 | + font_size=30, |
| 202 | + font_color=(0, 0, 255), |
| 203 | + background_color=(255, 255, 0)) |
| 204 | + |
| 205 | + # Display custom text with transparent background |
| 206 | + DisplayText(lcd_comm, "Transparent bold text", 5, 300, |
| 207 | + font="geforce/GeForce-Bold.ttf", |
| 208 | + font_size=30, |
| 209 | + font_color=(255, 255, 255), |
| 210 | + background_image="res/example.png") |
| 211 | + |
| 212 | + # Display text that overflows |
| 213 | + DisplayText(lcd_comm, "Text overflow!", 5, 430, |
| 214 | + font="roboto/Roboto-Bold.ttf", |
| 215 | + font_size=60, |
| 216 | + font_color=(255, 255, 255), |
| 217 | + background_image="res/example.png") |
| 218 | + |
| 219 | + # Display the current time and some progress bars as fast as possible |
| 220 | + bar_value = 0 |
| 221 | + while not stop: |
| 222 | + DisplayText(lcd_comm, str(datetime.now().time()), 160, 2, |
| 223 | + font="roboto/Roboto-Bold.ttf", |
| 224 | + font_size=20, |
| 225 | + font_color=(255, 0, 0), |
| 226 | + background_image="res/example.png") |
| 227 | + |
| 228 | + DisplayProgressBar(lcd_comm, 10, 40, |
| 229 | + width=140, height=30, |
| 230 | + min_value=0, max_value=100, value=bar_value, |
| 231 | + bar_color=(255, 255, 0), bar_outline=True, |
| 232 | + background_image="res/example.png") |
| 233 | + |
| 234 | + DisplayProgressBar(lcd_comm, 160, 40, |
| 235 | + width=140, height=30, |
| 236 | + min_value=0, max_value=19, value=bar_value % 20, |
| 237 | + bar_color=(0, 255, 0), bar_outline=False, |
| 238 | + background_image="res/example.png") |
| 239 | + |
| 240 | + bar_value = (bar_value + 2) % 101 |
| 241 | + |
| 242 | + lcd_comm.close() |
0 commit comments