smartscreen-driver 0.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,234 @@
1
+ # turing-smart-screen-python - a Python system monitor and library for USB-C displays like Turing Smart Screen or XuanFang
2
+ # https://github.com/mathoudebine/turing-smart-screen-python/
3
+
4
+ # Copyright (C) 2021-2023 Matthieu Houdebine (mathoudebine)
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+
19
+ import queue
20
+ import threading
21
+ import time
22
+ from abc import ABC, abstractmethod
23
+ from enum import IntEnum
24
+ from typing import Tuple, Optional
25
+ import logging
26
+
27
+ import serial
28
+ from PIL import Image
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class Orientation(IntEnum):
34
+ PORTRAIT = 0
35
+ LANDSCAPE = 2
36
+ REVERSE_PORTRAIT = 1
37
+ REVERSE_LANDSCAPE = 3
38
+
39
+
40
+ class ComPortDetectError(Exception):
41
+ pass
42
+
43
+
44
+ class LcdComm(ABC):
45
+ def __init__(
46
+ self,
47
+ com_port: str = "AUTO",
48
+ display_width: int = 320,
49
+ display_height: int = 480,
50
+ update_queue: Optional[queue.Queue] = None,
51
+ ):
52
+ self.lcd_serial = None
53
+
54
+ # String containing absolute path to serial port e.g. "COM3", "/dev/ttyACM1" or "AUTO" for auto-discovery
55
+ self.com_port = com_port
56
+
57
+ # Display always start in portrait orientation by default
58
+ self.orientation = Orientation.PORTRAIT
59
+ # Display width in default orientation (portrait)
60
+ self.display_width = display_width
61
+ # Display height in default orientation (portrait)
62
+ self.display_height = display_height
63
+
64
+ # Queue containing the serial requests to send to the screen. An external thread should run to process requests
65
+ # on the queue. If you want serial requests to be done in sequence, set it to None
66
+ self.update_queue = update_queue
67
+
68
+ # Mutex to protect the queue in case a thread want to add multiple requests (e.g. image data) that should not be
69
+ # mixed with other requests in-between
70
+ self.update_queue_mutex = threading.Lock()
71
+
72
+ def width(self) -> int:
73
+ if (
74
+ self.orientation == Orientation.PORTRAIT
75
+ or self.orientation == Orientation.REVERSE_PORTRAIT
76
+ ):
77
+ return self.display_width
78
+ else:
79
+ return self.display_height
80
+
81
+ def height(self) -> int:
82
+ if (
83
+ self.orientation == Orientation.PORTRAIT
84
+ or self.orientation == Orientation.REVERSE_PORTRAIT
85
+ ):
86
+ return self.display_height
87
+ else:
88
+ return self.display_width
89
+
90
+ def size(self) -> Tuple[int, int]:
91
+ return self.width(), self.height()
92
+
93
+ def open_serial(self):
94
+ if self.com_port == "AUTO":
95
+ self.com_port = self.auto_detect_com_port()
96
+ if not self.com_port:
97
+ raise ComPortDetectError("No COM port detected")
98
+ else:
99
+ logger.debug(f"Auto detected COM port: {self.com_port}")
100
+ else:
101
+ logger.debug(f"Static COM port: {self.com_port}")
102
+
103
+ self.lcd_serial = serial.Serial(self.com_port, 115200, timeout=1, rtscts=True)
104
+
105
+ def close_serial(self):
106
+ if self.lcd_serial is not None:
107
+ self.lcd_serial.close()
108
+
109
+ def serial_write(self, data: bytes):
110
+ assert self.lcd_serial is not None
111
+ self.lcd_serial.write(data)
112
+
113
+ def serial_read(self, size: int) -> bytes:
114
+ assert self.lcd_serial is not None
115
+ return self.lcd_serial.read(size)
116
+
117
+ def serial_flush_input(self):
118
+ if self.lcd_serial is not None:
119
+ self.lcd_serial.reset_input_buffer()
120
+
121
+ def write_data(self, data: bytearray):
122
+ self.write_line(bytes(data))
123
+
124
+ def send_line(self, line: bytes):
125
+ if self.update_queue:
126
+ # Queue the request. Mutex is locked by caller to queue multiple lines
127
+ self.update_queue.put((self.write_line, [line]))
128
+ else:
129
+ # If no queue for async requests: do request now
130
+ self.write_line(line)
131
+
132
+ def write_line(self, line: bytes):
133
+ try:
134
+ self.serial_write(line)
135
+ except serial.SerialTimeoutException:
136
+ # We timed-out trying to write to our device, slow things down.
137
+ logger.warning("(Write line) Too fast! Slow down!")
138
+ except serial.SerialException:
139
+ # Error writing data to device: close and reopen serial port, try to write again
140
+ logger.error(
141
+ "SerialException: Failed to send serial data to device. Closing and reopening COM port before retrying once."
142
+ )
143
+ self.close_serial()
144
+ time.sleep(1)
145
+ self.open_serial()
146
+ self.serial_write(line)
147
+
148
+ def read_data(self, size: int):
149
+ try:
150
+ response = self.serial_read(size)
151
+ # logger.debug("Received: [{}]".format(str(response, 'utf-8')))
152
+ return response
153
+ except serial.SerialTimeoutException:
154
+ # We timed-out trying to read from our device, slow things down.
155
+ logger.warning("(Read data) Too fast! Slow down!")
156
+ except serial.SerialException:
157
+ # Error writing data to device: close and reopen serial port, try to read again
158
+ logger.error(
159
+ "SerialException: Failed to read serial data from device. Closing and reopening COM port before retrying once."
160
+ )
161
+ self.close_serial()
162
+ time.sleep(1)
163
+ self.open_serial()
164
+ return self.serial_read(size)
165
+
166
+ @staticmethod
167
+ @abstractmethod
168
+ def auto_detect_com_port() -> Optional[str]:
169
+ pass
170
+
171
+ @abstractmethod
172
+ def initialize_comm(self):
173
+ pass
174
+
175
+ @abstractmethod
176
+ def reset(self):
177
+ pass
178
+
179
+ @abstractmethod
180
+ def clear(self):
181
+ pass
182
+
183
+ @abstractmethod
184
+ def screen_off(self):
185
+ pass
186
+
187
+ @abstractmethod
188
+ def screen_on(self):
189
+ pass
190
+
191
+ @abstractmethod
192
+ def set_brightness(self, level: int):
193
+ pass
194
+
195
+ def set_backplate_led_color(
196
+ self, led_color: Tuple[int, int, int] = (255, 255, 255)
197
+ ):
198
+ pass
199
+
200
+ @abstractmethod
201
+ def set_orientation(self, orientation: Orientation):
202
+ pass
203
+
204
+ def _crop_to_display_bounds(
205
+ self,
206
+ image: Image.Image,
207
+ pos: Tuple[int, int] = (0, 0),
208
+ ) -> Image.Image:
209
+ width, height = self.size()
210
+ x, y = pos
211
+ image_width, image_height = image.size[0], image.size[1]
212
+
213
+ assert 0 <= x < width, "x position not within display bounds"
214
+ assert 0 <= y < height, "y position not within display bounds"
215
+
216
+ # If our image size + the (x, y) position offsets are bigger than
217
+ # our display, reduce the image size to fit our screen
218
+ if x + image_width > width:
219
+ image_width = width - x
220
+ if y + image_height > height:
221
+ image_height = height - y
222
+
223
+ if image_width != image.size[0] or image_height != image.size[1]:
224
+ image = image.crop((0, 0, image_width, image_height))
225
+
226
+ return image
227
+
228
+ @abstractmethod
229
+ def paint(
230
+ self,
231
+ image: Image.Image,
232
+ pos: Tuple[int, int] = (0, 0),
233
+ ):
234
+ pass
@@ -0,0 +1,225 @@
1
+ # turing-smart-screen-python - a Python system monitor and library for USB-C displays like Turing Smart Screen or XuanFang
2
+ # https://github.com/mathoudebine/turing-smart-screen-python/
3
+
4
+ # Copyright (C) 2021-2023 Matthieu Houdebine (mathoudebine)
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <https://www.gnu.org/licenses/>.
18
+
19
+ import time
20
+ import queue
21
+ from enum import Enum, IntEnum
22
+ from typing import Optional, Tuple
23
+ import logging
24
+
25
+ from serial.tools.list_ports import comports
26
+ from PIL import Image
27
+
28
+ from .lcd_comm import LcdComm, Orientation
29
+ from .serialize import image_to_rgb565, chunked
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class Command(IntEnum):
35
+ RESET = 101 # Resets the display
36
+ CLEAR = 102 # Clears the display to a white screen
37
+ TO_BLACK = 103 # Clears the display to a black screen. Works on Official Turing 3.5, may not work on other models
38
+ SCREEN_OFF = 108 # Turns the screen off
39
+ SCREEN_ON = 109 # Turns the screen on
40
+ SET_BRIGHTNESS = 110 # Sets the screen brightness
41
+ SET_ORIENTATION = 121 # Sets the screen orientation
42
+ DISPLAY_BITMAP = 197 # Displays an image on the screen
43
+
44
+ # Commands below are only supported by next generation Turing Smart screens
45
+ LCD_28 = 40 # ?
46
+ LCD_29 = 41 # ?
47
+ HELLO = 69 # Asks the screen for its model: 3.5", 5" or 7"
48
+ SET_MIRROR = 122 # Mirrors the rendering on the screen
49
+ DISPLAY_PIXELS = 195 # Displays a list of pixels than can be non-contiguous in one command, useful for line charts
50
+
51
+
52
+ class SubRevision(Enum):
53
+ TURING_3_5 = 0 # Official Turing 3.5 do not answer to HELLO command
54
+ USBMONITOR_3_5 = bytearray([0x01, 0x01, 0x01, 0x01, 0x01, 0x01])
55
+ USBMONITOR_5 = bytearray([0x02, 0x02, 0x02, 0x02, 0x02, 0x02])
56
+ USBMONITOR_7 = bytearray([0x03, 0x03, 0x03, 0x03, 0x03, 0x03])
57
+
58
+
59
+ # This class is for Turing Smart Screen (rev. A) 3.5" and UsbMonitor screens (all sizes)
60
+ class LcdCommRevA(LcdComm):
61
+ def __init__(
62
+ self,
63
+ com_port: str = "AUTO",
64
+ display_width: int = 320,
65
+ display_height: int = 480,
66
+ update_queue: Optional[queue.Queue] = None,
67
+ ):
68
+ logger.debug("HW revision: A")
69
+ LcdComm.__init__(self, com_port, display_width, display_height, update_queue)
70
+ self.open_serial()
71
+
72
+ def __del__(self):
73
+ self.close_serial()
74
+
75
+ @staticmethod
76
+ def auto_detect_com_port() -> Optional[str]:
77
+ com_ports = comports()
78
+ auto_com_port = None
79
+
80
+ for com_port in com_ports:
81
+ if com_port.serial_number == "USB35INCHIPSV2":
82
+ auto_com_port = com_port.device
83
+ break
84
+
85
+ return auto_com_port
86
+
87
+ def send_command(
88
+ self, cmd: Command, x: int, y: int, ex: int, ey: int, bypass_queue: bool = False
89
+ ):
90
+ byte_buffer = bytearray(6)
91
+ byte_buffer[0] = x >> 2
92
+ byte_buffer[1] = ((x & 3) << 6) + (y >> 4)
93
+ byte_buffer[2] = ((y & 15) << 4) + (ex >> 6)
94
+ byte_buffer[3] = ((ex & 63) << 2) + (ey >> 8)
95
+ byte_buffer[4] = ey & 255
96
+ byte_buffer[5] = cmd
97
+
98
+ # If no queue for async requests, or if asked explicitly to do the request sequentially: do request now
99
+ if not self.update_queue or bypass_queue:
100
+ self.write_data(byte_buffer)
101
+ else:
102
+ # Lock queue mutex then queue the request
103
+ with self.update_queue_mutex:
104
+ self.update_queue.put((self.write_data, [byte_buffer]))
105
+
106
+ def _hello(self):
107
+ hello = bytearray(
108
+ [
109
+ Command.HELLO,
110
+ Command.HELLO,
111
+ Command.HELLO,
112
+ Command.HELLO,
113
+ Command.HELLO,
114
+ Command.HELLO,
115
+ ]
116
+ )
117
+
118
+ # This command reads LCD answer on serial link, so it bypasses the queue
119
+ self.write_data(hello)
120
+ response = self.serial_read(6)
121
+ self.serial_flush_input()
122
+
123
+ if response == SubRevision.USBMONITOR_3_5.value:
124
+ self.sub_revision = SubRevision.USBMONITOR_3_5
125
+ self.display_width = 320
126
+ self.display_height = 480
127
+ elif response == SubRevision.USBMONITOR_5.value:
128
+ self.sub_revision = SubRevision.USBMONITOR_5
129
+ self.display_width = 480
130
+ self.display_height = 800
131
+ elif response == SubRevision.USBMONITOR_7.value:
132
+ self.sub_revision = SubRevision.USBMONITOR_7
133
+ self.display_width = 600
134
+ self.display_height = 1024
135
+ else:
136
+ self.sub_revision = SubRevision.TURING_3_5
137
+ self.display_width = 320
138
+ self.display_height = 480
139
+
140
+ logger.debug("HW sub-revision: %s" % (str(self.sub_revision)))
141
+
142
+ def initialize_comm(self):
143
+ self._hello()
144
+
145
+ def reset(self):
146
+ logger.info("Display reset (COM port may change)...")
147
+ # Reset command bypasses queue because it is run when queue threads are not yet started
148
+ self.send_command(Command.RESET, 0, 0, 0, 0, bypass_queue=True)
149
+ self.close_serial()
150
+ # Wait for display reset then reconnect
151
+ time.sleep(5)
152
+ self.open_serial()
153
+
154
+ def clear(self):
155
+ self.set_orientation(
156
+ Orientation.PORTRAIT
157
+ ) # Bug: orientation needs to be PORTRAIT before clearing
158
+ if self.sub_revision == SubRevision.TURING_3_5:
159
+ self.send_command(Command.TO_BLACK, 0, 0, 0, 0)
160
+ else:
161
+ self.send_command(Command.CLEAR, 0, 0, 0, 0)
162
+ self.set_orientation() # Restore default orientation
163
+
164
+ def screen_off(self):
165
+ self.send_command(Command.SCREEN_OFF, 0, 0, 0, 0)
166
+
167
+ def screen_on(self):
168
+ self.send_command(Command.SCREEN_ON, 0, 0, 0, 0)
169
+
170
+ def set_brightness(self, level: int = 25):
171
+ assert 0 <= level <= 100, "Brightness level must be [0-100]"
172
+
173
+ # Display scales from 0 to 255, with 0 being the brightest and 255 being the darkest.
174
+ # Convert our brightness % to an absolute value.
175
+ level_absolute = int(255 - ((level / 100) * 255))
176
+
177
+ # Level : 0 (brightest) - 255 (darkest)
178
+ self.send_command(Command.SET_BRIGHTNESS, level_absolute, 0, 0, 0)
179
+
180
+ def set_orientation(self, orientation: Orientation = Orientation.PORTRAIT):
181
+ self.orientation = orientation
182
+ width = self.width()
183
+ height = self.height()
184
+ x = 0
185
+ y = 0
186
+ ex = 0
187
+ ey = 0
188
+ byte_buffer = bytearray(16)
189
+ byte_buffer[0] = x >> 2
190
+ byte_buffer[1] = ((x & 3) << 6) + (y >> 4)
191
+ byte_buffer[2] = ((y & 15) << 4) + (ex >> 6)
192
+ byte_buffer[3] = ((ex & 63) << 2) + (ey >> 8)
193
+ byte_buffer[4] = ey & 255
194
+ byte_buffer[5] = Command.SET_ORIENTATION
195
+ byte_buffer[6] = orientation + 100
196
+ byte_buffer[7] = width >> 8
197
+ byte_buffer[8] = width & 255
198
+ byte_buffer[9] = height >> 8
199
+ byte_buffer[10] = height & 255
200
+ self.serial_write(bytes(byte_buffer))
201
+
202
+ def paint(
203
+ self,
204
+ image: Image.Image,
205
+ pos: Tuple[int, int] = (0, 0),
206
+ ):
207
+ image = self._crop_to_display_bounds(image, pos)
208
+ image_width, image_height = image.size[0], image.size[1]
209
+
210
+ if image_height == 0 or image_width == 0:
211
+ return
212
+
213
+ x, y = pos
214
+ x1, y1 = x + image_width - 1, y + image_height - 1
215
+
216
+ rgb565le = image_to_rgb565(image, "little")
217
+
218
+ self.send_command(Command.DISPLAY_BITMAP, x, y, x1, y1)
219
+
220
+ # Lock queue mutex then queue all the requests for the image data
221
+ with self.update_queue_mutex:
222
+ width = self.width()
223
+ # Send image data by multiple of "display width" bytes
224
+ for chunk in chunked(rgb565le, width * 8):
225
+ self.send_line(chunk)