Skip to content

Commit 46dbfdf

Browse files
committed
Refactoring: remove all dependencies to CONFIG/THEME from LcdComm classes, so they can be used in standalone.
Adding simple-program.py (inspired by old main.py from 1.2.0) to show how to include LcdComm in external projects
1 parent 4e0d974 commit 46dbfdf

File tree

12 files changed

+302
-162
lines changed

12 files changed

+302
-162
lines changed

README.md

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# turing-smart-screen-python
22

3+
| Check out new version with system monitoring features! |
4+
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
5+
| Are you using your Turing Smart Screen for system monitoring? <br>If so, check out the new [**pre-release 2.0.0 beta 1 - 📊 System Monitor**](https://github.com/mathoudebine/turing-smart-screen-python/releases/tag/2.0.0-beta.1) or the `feature/system-monitoring` branch! <br><img src="res/pics/Theme3.5Inch.jpg" height="600" /> <img src="res/pics/ThemeTerminal.jpg" height="600" /> <br>It contains embedded hardware monitoring functions, theme creation from configuration files, serial port auto-detection... <br>See Release Notes to learn more about features and current limitations <br>_Python knowledges recommended._ |
6+
37
---
8+
49
### ⚠️ DISCLAIMER - PLEASE READ ⚠️
510

611
This project is **not affiliated, associated, authorized, endorsed by, or in any way officially connected with Turing brand**, or any of its subsidiaries, affiliates, manufacturers or sellers of the Turing products. All product and company names are the registered trademarks of their original owners.
@@ -10,40 +15,41 @@ This project is an open-source alternative software, not the USBMonitor.exe orig
1015
---
1116

1217
A simple Python manager for "Turing Smart Screen" 3.5" IPS USB-C (UART) display, also known as :
13-
- Turing USB35INCHIPS / USB35INCHIPSV2
14-
- 3.5 Inch Mini Screen
15-
- [3.5 Inch 320*480 Mini Capacitive Touch Screen IPS Module](https://www.aliexpress.com/item/1005002505149293.html)
18+
- Turing USB35INCHIPS / USB35INCHIPSV2 (revision A)
19+
- XuanFang display (revision B & flagship)
20+
- [3.5 Inch 320*480 Mini Capacitive Touch Screen IPS Module](https://www.aliexpress.com/item/1005003723773653.html)
1621

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

21-
This is a 3.5" USB-C display that shows as a serial port once connected.
22-
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.
2327

24-
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.
2530
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.
2631

2732
## Features
2833
This Python script can do some simple operations on the Turing display like :
2934
- **Display custom picture**
3035
- **Display text**
3136
- **Display progress bar**
32-
- Clear the screen (blank)
33-
- Turn the screen on/off
34-
- Display soft reset
37+
- **Screen rotation**
38+
- Clear the screen (blank) - HW version A only
39+
- Turn the screen on/off - HW version A only
40+
- Display soft reset - HW version A only
3541
- Set brightness
3642

37-
Not yet implemented:
38-
- Screen rotation
43+
Operating systems supported : macOS, Windows, Linux (incl. Raspberry Pi) and all OS that support Python3.7
3944

4045
## Getting started
4146
_Python knowledges recommended._
42-
Download the `main.py` file from this project
43-
Download and install latest Python 3.x for your OS: https://www.python.org/downloads/
47+
Download this project by cloning it or using the [Releases sections](https://github.com/mathoudebine/turing-smart-screen-python/releases)
48+
Download and install the latest Python 3.x (min. 3.7) for your OS: https://www.python.org/downloads/
4449
Plug your Turing display to your computer (install the drivers if on Windows)
45-
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
50+
[Identify your hardware revision (version A or version B/flagship)](https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions)
51+
Open the `config.yaml` file and change display settings if needed.
4652
Open a terminal and run `python3 main.py` or `py -3 main.py` depending on your OS
4753
You should see animated content on your Turing display!
4854

49-
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
55+
For a simple program to control display (e.g. from your personal Python project), you can look at `simple-program.py`

config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ display:
1919

2020
# Display Brightness
2121
# Set this as the desired %, 0 being completely dark and 100 being max brightness
22+
# Warning: screen can get very hot at high brightness!
2223
BRIGHTNESS: 20
2324

2425
# Display revision: A or B (for "flagship" version, use B)

library/config.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import os
22
import queue
33
import sys
4-
import threading
54

65
import yaml
76

@@ -31,7 +30,3 @@ def load_yaml(configfile):
3130

3231
# Queue containing the serial requests to send to the screen
3332
update_queue = queue.Queue()
34-
35-
# Mutex to protect the queue in case a thread want to add multiple requests (e.g. image data) that should not be
36-
# mixed with other requests in-between
37-
update_queue_mutex = threading.Lock()

library/display.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from library import config
2+
from library.lcd_comm import Orientation
23
from library.lcd_comm_rev_a import LcdCommRevA
34
from library.lcd_comm_rev_b import LcdCommRevB
45
from library.log import logger
@@ -7,20 +8,40 @@
78
CONFIG_DATA = config.CONFIG_DATA
89

910

10-
def get_full_path(path, name):
11+
def _get_full_path(path, name):
1112
if name:
1213
return path + name
1314
else:
1415
return None
1516

1617

18+
def _get_theme_orientation() -> Orientation:
19+
if THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'portrait':
20+
return Orientation.PORTRAIT
21+
elif THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'landscape':
22+
return Orientation.LANDSCAPE
23+
elif THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'reverse_portrait':
24+
return Orientation.REVERSE_PORTRAIT
25+
elif THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'reverse_landscape':
26+
return Orientation.REVERSE_LANDSCAPE
27+
else:
28+
logger.warning("Orientation '", THEME_DATA["display"]["DISPLAY_ORIENTATION"], "' unknown, using portrait")
29+
return Orientation.PORTRAIT
30+
31+
1732
class Display:
1833
def __init__(self):
1934
self.lcd = None
2035
if CONFIG_DATA["display"]["REVISION"] == "A":
21-
self.lcd = LcdCommRevA()
36+
self.lcd = LcdCommRevA(com_port=CONFIG_DATA['config']['COM_PORT'],
37+
display_width=CONFIG_DATA["display"]["DISPLAY_WIDTH"],
38+
display_height=CONFIG_DATA["display"]["DISPLAY_HEIGHT"],
39+
update_queue=config.update_queue)
2240
elif CONFIG_DATA["display"]["REVISION"] == "B":
23-
self.lcd = LcdCommRevB()
41+
self.lcd = LcdCommRevB(com_port=CONFIG_DATA['config']['COM_PORT'],
42+
display_width=CONFIG_DATA["display"]["DISPLAY_WIDTH"],
43+
display_height=CONFIG_DATA["display"]["DISPLAY_HEIGHT"],
44+
update_queue=config.update_queue)
2445
else:
2546
logger.error("Unknown display revision '", CONFIG_DATA["display"]["REVISION"], "'")
2647

@@ -32,13 +53,13 @@ def initialize_display(self):
3253
self.lcd.InitializeComm()
3354

3455
# Set brightness
35-
self.lcd.SetBrightness()
56+
self.lcd.SetBrightness(CONFIG_DATA["display"]["BRIGHTNESS"])
3657

3758
# Set backplate RGB LED color (for supported HW only)
38-
self.lcd.SetBackplateLedColor()
59+
self.lcd.SetBackplateLedColor(THEME_DATA['display']["DISPLAY_RGB_LED"])
3960

4061
# Set orientation
41-
self.lcd.SetOrientation()
62+
self.lcd.SetOrientation(_get_theme_orientation())
4263

4364
def display_static_images(self):
4465
if THEME_DATA['static_images']:
@@ -64,8 +85,8 @@ def display_static_text(self):
6485
font_size=THEME_DATA['static_text'][text].get("FONT_SIZE", 10),
6586
font_color=THEME_DATA['static_text'][text].get("FONT_COLOR", (0, 0, 0)),
6687
background_color=THEME_DATA['static_text'][text].get("BACKGROUND_COLOR", (255, 255, 255)),
67-
background_image=get_full_path(THEME_DATA['PATH'],
68-
THEME_DATA['static_text'][text].get("BACKGROUND_IMAGE", None))
88+
background_image=_get_full_path(THEME_DATA['PATH'],
89+
THEME_DATA['static_text'][text].get("BACKGROUND_IMAGE", None))
6990
)
7091

7192

library/lcd_comm.py

Lines changed: 76 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1+
import queue
2+
import threading
13
from abc import ABC, abstractmethod
24
from enum import IntEnum
35

46
import serial
57
from PIL import Image, ImageDraw, ImageFont
68

7-
from library import config
89
from library.log import logger
910

10-
CONFIG_DATA = config.CONFIG_DATA
11-
THEME_DATA = config.THEME_DATA
12-
1311

1412
class Orientation(IntEnum):
1513
PORTRAIT = 0
@@ -18,45 +16,79 @@ class Orientation(IntEnum):
1816
REVERSE_LANDSCAPE = 3
1917

2018

21-
def get_theme_orientation() -> Orientation:
22-
if THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'portrait':
23-
return Orientation.PORTRAIT
24-
elif THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'landscape':
25-
return Orientation.LANDSCAPE
26-
elif THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'reverse_portrait':
27-
return Orientation.REVERSE_PORTRAIT
28-
elif THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'reverse_landscape':
29-
return Orientation.REVERSE_LANDSCAPE
30-
else:
31-
logger.warning("Orientation '", THEME_DATA["display"]["DISPLAY_ORIENTATION"], "' unknown, using portrait")
32-
return Orientation.PORTRAIT
33-
34-
35-
def get_width() -> int:
36-
if get_theme_orientation() == Orientation.PORTRAIT or get_theme_orientation() == Orientation.REVERSE_PORTRAIT:
37-
return CONFIG_DATA["display"]["DISPLAY_WIDTH"]
38-
else:
39-
return CONFIG_DATA["display"]["DISPLAY_HEIGHT"]
40-
41-
42-
def get_height() -> int:
43-
if get_theme_orientation() == Orientation.PORTRAIT or get_theme_orientation() == Orientation.REVERSE_PORTRAIT:
44-
return CONFIG_DATA["display"]["DISPLAY_HEIGHT"]
45-
else:
46-
return CONFIG_DATA["display"]["DISPLAY_WIDTH"]
19+
class LcdComm(ABC):
20+
def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_height: int = 480,
21+
update_queue: queue.Queue = None):
22+
self.lcd_serial = None
23+
24+
# String containing absolute path to serial port e.g. "COM3", "/dev/ttyACM1" or "AUTO" for auto-discovery
25+
self.com_port = com_port
26+
27+
# Display always start in portrait orientation by default
28+
self.orientation = Orientation.PORTRAIT
29+
# Display width in default orientation (portrait)
30+
self.display_width = display_width
31+
# Display height in default orientation (portrait)
32+
self.display_height = display_height
33+
34+
# Queue containing the serial requests to send to the screen. An external thread should run to process requests
35+
# on the queue. If you want serial requests to be done in sequence, set it to None
36+
self.update_queue = update_queue
37+
38+
# Mutex to protect the queue in case a thread want to add multiple requests (e.g. image data) that should not be
39+
# mixed with other requests in-between
40+
self.update_queue_mutex = threading.Lock()
41+
42+
def get_width(self) -> int:
43+
if self.orientation == Orientation.PORTRAIT or self.orientation == Orientation.REVERSE_PORTRAIT:
44+
return self.display_width
45+
else:
46+
return self.display_height
4747

48+
def get_height(self) -> int:
49+
if self.orientation == Orientation.PORTRAIT or self.orientation == Orientation.REVERSE_PORTRAIT:
50+
return self.display_height
51+
else:
52+
return self.display_width
4853

49-
class LcdComm(ABC):
5054
def openSerial(self):
51-
if CONFIG_DATA['config']['COM_PORT'] == 'AUTO':
55+
if self.com_port == 'AUTO':
5256
lcd_com_port = self.auto_detect_com_port()
5357
self.lcd_serial = serial.Serial(lcd_com_port, 115200, timeout=1, rtscts=1)
54-
logger.debug(f"Auto detected comm port: {lcd_com_port}")
58+
logger.debug(f"Auto detected COM port: {lcd_com_port}")
5559
else:
56-
lcd_com_port = CONFIG_DATA["config"]["COM_PORT"]
57-
logger.debug(f"Static comm port: {lcd_com_port}")
60+
lcd_com_port = self.com_port
61+
logger.debug(f"Static COM port: {lcd_com_port}")
5862
self.lcd_serial = serial.Serial(lcd_com_port, 115200, timeout=1, rtscts=1)
5963

64+
def closeSerial(self):
65+
try:
66+
self.lcd_serial.close()
67+
except:
68+
pass
69+
70+
def WriteData(self, byteBuffer: bytearray):
71+
try:
72+
self.lcd_serial.write(bytes(byteBuffer))
73+
except serial.serialutil.SerialTimeoutException:
74+
# We timed-out trying to write to our device, slow things down.
75+
logger.warning("(Write data) Too fast! Slow down!")
76+
77+
def SendLine(self, line: bytes):
78+
if self.update_queue:
79+
# Queue the request. Mutex is locked by caller to queue multiple lines
80+
self.update_queue.put((self.WriteLine, [line]))
81+
else:
82+
# If no queue for async requests: do request now
83+
self.WriteLine(line)
84+
85+
def WriteLine(self, line: bytes):
86+
try:
87+
self.lcd_serial.write(line)
88+
except serial.serialutil.SerialTimeoutException:
89+
# We timed-out trying to write to our device, slow things down.
90+
logger.warning("(Write line) Too fast! Slow down!")
91+
6092
@staticmethod
6193
@abstractmethod
6294
def auto_detect_com_port():
@@ -128,16 +160,16 @@ def DisplayText(
128160
if isinstance(background_color, str):
129161
background_color = tuple(map(int, background_color.split(', ')))
130162

131-
assert x <= get_width(), 'Text X coordinate must be <= display width'
132-
assert y <= get_height(), 'Text Y coordinate must be <= display height'
163+
assert x <= self.get_width(), 'Text X coordinate must be <= display width'
164+
assert y <= self.get_height(), 'Text Y coordinate must be <= display height'
133165
assert len(text) > 0, 'Text must not be empty'
134166
assert font_size > 0, "Font size must be > 0"
135167

136168
if background_image is None:
137169
# A text bitmap is created with max width/height by default : text with solid background
138170
text_image = Image.new(
139171
'RGB',
140-
(get_width(), get_height()),
172+
(self.get_width(), self.get_height()),
141173
background_color
142174
)
143175
else:
@@ -153,8 +185,8 @@ def DisplayText(
153185
left, top, text_width, text_height = d.textbbox((0, 0), text, font=font)
154186
text_image = text_image.crop(box=(
155187
x, y,
156-
min(x + text_width, get_width()),
157-
min(y + text_height, get_height())
188+
min(x + text_width, self.get_width()),
189+
min(y + text_height, self.get_height())
158190
))
159191

160192
self.DisplayPILImage(text_image, x, y)
@@ -174,10 +206,10 @@ def DisplayProgressBar(self, x: int, y: int, width: int, height: int, min_value:
174206
if isinstance(background_color, str):
175207
background_color = tuple(map(int, background_color.split(', ')))
176208

177-
assert x <= get_width(), 'Progress bar X coordinate must be <= display width'
178-
assert y <= get_height(), 'Progress bar Y coordinate must be <= display height'
179-
assert x + width <= get_width(), 'Progress bar width exceeds display width'
180-
assert y + height <= get_height(), 'Progress bar height exceeds display height'
209+
assert x <= self.get_width(), 'Progress bar X coordinate must be <= display width'
210+
assert y <= self.get_height(), 'Progress bar Y coordinate must be <= display height'
211+
assert x + width <= self.get_width(), 'Progress bar width exceeds display width'
212+
assert y + height <= self.get_height(), 'Progress bar height exceeds display height'
181213

182214
# Don't let the set value exceed our min or max value, this is bad :)
183215
if value < min_value:

0 commit comments

Comments
 (0)