Skip to content

Commit 3462f7a

Browse files
authored
Merge pull request #28 from mathoudebine/feature/24-different-smart-screen-implementation-in-newer-models
Feature/24 different smart screen implementation in newer models
2 parents c8ecaa8 + 46720b8 commit 3462f7a

File tree

3 files changed

+320
-14
lines changed

3 files changed

+320
-14
lines changed

README.md

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,38 +17,41 @@ This project is an open-source alternative software, not the USBMonitor.exe orig
1717
A simple Python manager for "Turing Smart Screen" 3.5" IPS USB-C (UART) display, also known as :
1818
- Turing USB35INCHIPS / USB35INCHIPSV2
1919
- 3.5 Inch Mini Screen
20-
- [3.5 Inch 320*480 Mini Capacitive Touch Screen IPS Module](https://www.aliexpress.com/item/1005002505149293.html)
20+
- [3.5 Inch 320*480 Mini Capacitive Touch Screen IPS Module](https://www.aliexpress.com/item/1005003723773653.html)
2121

22-
Operating systems supported : macOS, Windows, Linux (incl. Raspberry Pi) and all OS that support Python3.7
23-
22+
## Hardware
2423
<img src="res/pics/smart-screen-3.webp" width="500"/>
2524

26-
This is a 3.5" USB-C display that shows as a serial port once connected.
27-
It cannot be seen by the operating system as a monitor but picture can be displayed on it.
25+
The Turing Smart Screen is a 3.5" USB-C display that shows as a serial port once connected.
26+
It cannot be seen by the operating system as a monitor but pictures can be displayed on it.
2827

29-
A Windows-only software is [available in Chinese](https://lgb123-1253504678.cos.ap-beijing.myqcloud.com/35inch.rar) or [in English](https://lgb123-1253504678.cos.ap-beijing.myqcloud.com/35inchENG.rar) to manage this display.
28+
There is 3 hardware revisions of the screen: [how to identify my version?](https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions) Version B and "flagship" use the same protocol.
29+
A [Windows-only software is available](https://github.com/mathoudebine/turing-smart-screen-python/wiki/Vendor-apps) is provided by the vendor to manage this display.
3030
This software allows creating themes to display your computer sensors on the screen, but does not offer a simple way to display custom pictures or text.
3131

3232
## Features
3333
This Python script can do some simple operations on the Turing display like :
3434
- **Display custom picture**
3535
- **Display text**
3636
- **Display progress bar**
37-
- Clear the screen (blank)
38-
- Turn the screen on/off
39-
- Display soft reset
37+
- Clear the screen (blank) - HW version A only
38+
- Turn the screen on/off - HW version A only
39+
- Display soft reset - HW version A only
4040
- Set brightness
4141

4242
Not yet implemented:
4343
- Screen rotation
4444

45+
Operating systems supported : macOS, Windows, Linux (incl. Raspberry Pi) and all OS that support Python3.7
46+
4547
## Getting started
4648
_Python knowledges recommended._
47-
Download the `main.py` file from this project
48-
Download and install latest Python 3.x (min. 3.7) for your OS: https://www.python.org/downloads/
49+
Download this project by cloning it or using the [Releases sections](https://github.com/mathoudebine/turing-smart-screen-python/releases)
50+
Download and install the latest Python 3.x (min. 3.7) for your OS: https://www.python.org/downloads/
4951
Plug your Turing display to your computer (install the drivers if on Windows)
50-
Open the `main.py` file and edit the [`COM_PORT`](https://github.com/mathoudebine/turing-smart-screen-python/blob/deb0a60b772f2c5acef377f13b959632ca649f9f/main.py#L15) variable to the port used by the display
51-
Open a terminal and run `python3 main.py` or `py -3 main.py` depending on your OS
52+
[Identify your hardware revision (version A or version B/flagship)](https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions)
53+
Open the `mainVersionA.py` or `mainVersionB.py` file and edit the [`COM_PORT`](https://github.com/mathoudebine/turing-smart-screen-python/blob/deb0a60b772f2c5acef377f13b959632ca649f9f/main.py#L15) variable to the port used by the display
54+
Open a terminal and run `python3 mainVersionA.py / mainVersionB.py` or `py -3 mainVersionA.py / mainVersionB.py` depending on your OS
5255
You should see animated content on your Turing display!
5356

54-
You can then edit the `main.py` file to change the content displayed, or use this file as a Python module for your personal Python project
57+
You can then edit the `mainVersionA.py / mainVersionB.py` file to change the content displayed, or use this file as a Python module for your personal Python project

main.py renamed to mainVersionA.py

File renamed without changes.

mainVersionB.py

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
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+
# MacOS COM port:
18+
COM_PORT = '/dev/cu.usbmodem2017_2_251'
19+
20+
# The new device has a serial number of '2017-2-25'
21+
22+
DISPLAY_WIDTH = 320
23+
DISPLAY_HEIGHT = 480
24+
25+
26+
class TuringError(Exception):
27+
pass
28+
29+
30+
class Command:
31+
# New protocol (10 byte packets, framed with the command, 8 data bytes inside)
32+
HELLO = 0xCA
33+
ORIENTATION = 0xCB
34+
ORIENTATION_PORTRAIT = 0
35+
ORIENTATION_LANDSCAPE = 1
36+
# The device seems to start in PORTRAIT, with the row ordering reversed from
37+
# the ORIENTATION_PORTRAIT setting. It is not clear how to restore the ordering
38+
# to the reset configuration.
39+
DISPLAY_BITMAP = 0xCC
40+
LIGHTING = 0xCD
41+
SET_BRIGHTNESS = 0xCE
42+
43+
44+
def SendCommand(ser: serial.Serial, cmd: int, payload=None):
45+
if payload is None:
46+
payload = [0] * 8
47+
elif len(payload) < 8:
48+
payload = list(payload) + [0] * (8 - len(payload))
49+
50+
byteBuffer = bytearray(10)
51+
byteBuffer[0] = cmd
52+
byteBuffer[1] = payload[0]
53+
byteBuffer[2] = payload[1]
54+
byteBuffer[3] = payload[2]
55+
byteBuffer[4] = payload[3]
56+
byteBuffer[5] = payload[4]
57+
byteBuffer[6] = payload[5]
58+
byteBuffer[7] = payload[6]
59+
byteBuffer[8] = payload[7]
60+
byteBuffer[9] = cmd
61+
print("Sending %r" % (byteBuffer,))
62+
ser.write(bytes(byteBuffer))
63+
64+
65+
def Hello(ser: serial.Serial):
66+
hello = [ord('H'), ord('E'), ord('L'), ord('L'), ord('O')]
67+
SendCommand(ser, Command.HELLO, payload=hello)
68+
response = ser.read(10)
69+
if len(response) != 10:
70+
raise TuringError("Device not recognised (short response to HELLO)")
71+
if response[0] != Command.HELLO or response[-1] != Command.HELLO:
72+
raise TuringError("Device not recognised (bad framing)")
73+
if [x for x in response[1:6]] != hello:
74+
raise TuringError("Device not recognised (No HELLO; got %r)" % (response[1:6],))
75+
# The HELLO response here is followed by:
76+
# 0x0A, 0x12, 0x00
77+
# It is not clear what these might be.
78+
# It would be handy if these were a version number, or a set of capability
79+
# flags. The 0x0A=10 being version 10 or 0.10, and the 0x12 being the size or the
80+
# indication that a backlight is present, would be nice. But that's guessing
81+
# based on how I'd do it.
82+
83+
84+
def Orientation(ser: serial.Serial, state: int):
85+
print("Orientation: %r" % (state,))
86+
SendCommand(ser, Command.ORIENTATION, payload=[state])
87+
88+
89+
def SetLighting(ser: serial.Serial, red: int, green: int, blue: int):
90+
print("Lighting: %i, %i, %i" % (red, green, blue))
91+
assert red < 256, 'Red lighting must be < 256'
92+
assert green < 256, 'Green lighting must be < 256'
93+
assert blue < 256, 'Blue lighting must be < 256'
94+
SendCommand(ser, Command.LIGHTING, payload=[red, green, blue])
95+
96+
97+
def SetBrightness(ser: serial.Serial, level: int):
98+
# Level : 0 (brightest) - 255 (darkest)
99+
assert 255 >= level >= 0, 'Brightness level must be [0-255]'
100+
# New protocol has 255 as the brightest, and 0 as off.
101+
SendCommand(ser, Command.SET_BRIGHTNESS, payload=[255-level])
102+
103+
104+
def DisplayPILImage(ser: serial.Serial, image: Image, x: int, y: int):
105+
image_height = image.size[1]
106+
image_width = image.size[0]
107+
108+
assert image_height > 0, 'Image width must be > 0'
109+
assert image_width > 0, 'Image height must be > 0'
110+
111+
(x0, y0) = (x, y)
112+
(x1, y1) = (x + image_width - 1, y + image_height - 1)
113+
114+
SendCommand(ser, Command.DISPLAY_BITMAP,
115+
payload=[(x0>>8) & 255, x0 & 255,
116+
(y0>>8) & 255, y0 & 255,
117+
(x1>>8) & 255, x1 & 255,
118+
(y1>>8) & 255, y1 & 255])
119+
120+
pix = image.load()
121+
line = bytes()
122+
for h in range(image_height):
123+
for w in range(image_width):
124+
R = pix[w, h][0] >> 3
125+
G = pix[w, h][1] >> 2
126+
B = pix[w, h][2] >> 3
127+
128+
# Original: 0bRRRRRGGGGGGBBBBB
129+
# fedcba9876543210
130+
# New: 0bgggBBBBBRRRRRGGG
131+
# That is...
132+
# High 3 bits of green in b0-b2
133+
# Low 3 bits of green in b13-b15
134+
# Red 5 bits in b3-b7
135+
# Blue 5 bits in b8-b12
136+
rgb = (B << 8) | (G>>3) | ((G&7)<<13) | (R<<3)
137+
line += struct.pack('H', rgb)
138+
139+
# Send image data by multiple of DISPLAY_WIDTH bytes
140+
if len(line) >= DISPLAY_WIDTH * 8:
141+
ser.write(line)
142+
line = bytes()
143+
144+
# Write last line if needed
145+
if len(line) > 0:
146+
ser.write(line)
147+
148+
sleep(0.01) # Wait 10 ms after picture display
149+
150+
151+
def DisplayBitmap(ser: serial.Serial, bitmap_path: str, x=0, y=0):
152+
image = Image.open(bitmap_path)
153+
DisplayPILImage(ser, image, x, y)
154+
155+
156+
def DisplayText(ser: serial.Serial, text: str, x=0, y=0,
157+
font="roboto/Roboto-Regular.ttf",
158+
font_size=20,
159+
font_color=(0, 0, 0),
160+
background_color=(255, 255, 255),
161+
background_image: str = None):
162+
# Convert text to bitmap using PIL and display it
163+
# Provide the background image path to display text with transparent background
164+
165+
assert len(text) > 0, 'Text must not be empty'
166+
assert font_size > 0, "Font size must be > 0"
167+
168+
if background_image is None:
169+
# A text bitmap is created with max width/height by default : text with solid background
170+
text_image = Image.new('RGB', (DISPLAY_WIDTH, DISPLAY_HEIGHT), background_color)
171+
else:
172+
# The text bitmap is created from provided background image : text with transparent background
173+
text_image = Image.open(background_image)
174+
175+
# Draw text with specified color & font (also crop if text overflows display)
176+
font = ImageFont.truetype("./res/fonts/" + font, font_size)
177+
d = ImageDraw.Draw(text_image)
178+
d.text((x, y), text, font=font, fill=font_color)
179+
180+
# Crop text bitmap to keep only the text
181+
left, top, text_width, text_height = d.textbbox((0,0), text, font=font)
182+
text_image = text_image.crop(box=(x, y, min(x + text_width, DISPLAY_WIDTH), min(y + text_height, DISPLAY_HEIGHT)))
183+
184+
DisplayPILImage(ser, text_image, x, y)
185+
186+
187+
def DisplayProgressBar(ser: serial.Serial, x: int, y: int, width: int, height: int, min_value=0, max_value=100,
188+
value=50,
189+
bar_color=(0, 0, 0),
190+
bar_outline=True,
191+
background_color=(255, 255, 255),
192+
background_image: str = None):
193+
# Generate a progress bar and display it
194+
# Provide the background image path to display progress bar with transparent background
195+
196+
assert x + width <= DISPLAY_WIDTH, 'Progress bar width exceeds display width'
197+
assert y + height <= DISPLAY_HEIGHT, 'Progress bar height exceeds display height'
198+
assert min_value <= value <= max_value, 'Progress bar value shall be between min and max'
199+
200+
if background_image is None:
201+
# A bitmap is created with solid background
202+
bar_image = Image.new('RGB', (width, height), background_color)
203+
else:
204+
# A bitmap is created from provided background image
205+
bar_image = Image.open(background_image)
206+
207+
# Crop bitmap to keep only the progress bar background
208+
bar_image = bar_image.crop(box=(x, y, x + width, y + height))
209+
210+
# Draw progress bar
211+
bar_filled_width = value / (max_value - min_value) * width
212+
draw = ImageDraw.Draw(bar_image)
213+
draw.rectangle([0, 0, bar_filled_width-1, height-1], fill=bar_color, outline=bar_color)
214+
215+
if bar_outline:
216+
# Draw outline
217+
draw.rectangle([0, 0, width-1, height-1], fill=None, outline=bar_color)
218+
219+
DisplayPILImage(ser, bar_image, x, y)
220+
221+
222+
stop = False
223+
224+
if __name__ == "__main__":
225+
226+
def sighandler(signum, frame):
227+
global stop
228+
stop = True
229+
230+
231+
# Set the signal handlers, to send a complete frame to the LCD before exit
232+
signal.signal(signal.SIGINT, sighandler)
233+
signal.signal(signal.SIGTERM, sighandler)
234+
is_posix = os.name == 'posix'
235+
if is_posix:
236+
signal.signal(signal.SIGQUIT, sighandler)
237+
238+
# Do not change COM port settings unless you know what you are doing
239+
lcd_comm = serial.Serial(COM_PORT, 115200, timeout=1, rtscts=1)
240+
241+
# Hello! to check this is the right device
242+
Hello(lcd_comm)
243+
244+
# Data orientation
245+
Orientation(lcd_comm, Command.ORIENTATION_PORTRAIT)
246+
247+
# Set brightness to max value
248+
SetBrightness(lcd_comm, 0)
249+
250+
# Lighting (a purple)
251+
SetLighting(lcd_comm, 128, 50, 112)
252+
253+
# Display sample picture
254+
DisplayBitmap(lcd_comm, "res/example.png")
255+
256+
# Display sample text
257+
DisplayText(lcd_comm, "Basic text", 50, 100)
258+
259+
# Display custom text with solid background
260+
DisplayText(lcd_comm, "Custom italic text", 5, 150,
261+
font="roboto/Roboto-Italic.ttf",
262+
font_size=30,
263+
font_color=(0, 0, 255),
264+
background_color=(255, 255, 0))
265+
266+
# Display custom text with transparent background
267+
DisplayText(lcd_comm, "Transparent bold text", 5, 300,
268+
font="geforce/GeForce-Bold.ttf",
269+
font_size=30,
270+
font_color=(255, 255, 255),
271+
background_image="res/example.png")
272+
273+
# Display text that overflows
274+
DisplayText(lcd_comm, "Text overflow!", 5, 430,
275+
font="roboto/Roboto-Bold.ttf",
276+
font_size=60,
277+
font_color=(255, 255, 255),
278+
background_image="res/example.png")
279+
280+
# Display the current time and some progress bars as fast as possible
281+
bar_value = 0
282+
while not stop:
283+
DisplayText(lcd_comm, str(datetime.now().time()), 160, 2,
284+
font="roboto/Roboto-Bold.ttf",
285+
font_size=20,
286+
font_color=(255, 0, 0),
287+
background_image="res/example.png")
288+
289+
DisplayProgressBar(lcd_comm, 10, 40,
290+
width=140, height=30,
291+
min_value=0, max_value=100, value=bar_value,
292+
bar_color=(255, 255, 0), bar_outline=True,
293+
background_image="res/example.png")
294+
295+
DisplayProgressBar(lcd_comm, 160, 40,
296+
width=140, height=30,
297+
min_value=0, max_value=19, value=bar_value % 20,
298+
bar_color=(0, 255, 0), bar_outline=False,
299+
background_image="res/example.png")
300+
301+
bar_value = (bar_value + 2) % 101
302+
303+
lcd_comm.close()

0 commit comments

Comments
 (0)