smartscreen-driver 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,194 @@
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
+ from enum import Enum
20
+ import logging
21
+ from typing import Optional, Tuple
22
+ import queue
23
+
24
+ from serial.tools.list_ports import comports
25
+ from PIL import Image
26
+
27
+ from .lcd_comm import LcdComm, Orientation
28
+ from .serialize import image_to_rgb565, chunked
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class Command(Enum):
34
+ GETINFO = bytearray((71, 00, 00, 00))
35
+ SETORG = bytearray((67, 72, 00, 00)) # Set portrait orientation
36
+ SET180 = bytearray((67, 71, 00, 00)) # Set reverse portrait orientation
37
+ SETHF = bytearray(
38
+ (67, 68, 00, 00)
39
+ ) # Set portrait orientation with horizontal mirroring
40
+ SETVF = bytearray(
41
+ (67, 70, 00, 00)
42
+ ) # Set reverse portrait orientation with horizontal mirroring
43
+ SETBL = bytearray((67, 67)) # Brightness setting
44
+ DISPCOLOR = bytearray((67, 66)) # Display RGB565 color on whole screen
45
+ BLOCKWRITE = bytearray((67, 65)) # Send bitmap size
46
+ INTOPICMODE = bytearray((68, 00, 00, 00)) # Start bitmap transmission
47
+ OUTPICMODE = bytearray((65, 00, 00, 00)) # End bitmap transmission
48
+
49
+
50
+ # This class is for Kipye Qiye Smart Display 3.5"
51
+ class LcdCommRevD(LcdComm):
52
+ def __init__(
53
+ self,
54
+ com_port: str = "AUTO",
55
+ display_width: int = 320,
56
+ display_height: int = 480,
57
+ update_queue: Optional[queue.Queue] = None,
58
+ ):
59
+ logger.debug("HW revision: D")
60
+ LcdComm.__init__(self, com_port, display_width, display_height, update_queue)
61
+ self.open_serial()
62
+
63
+ def __del__(self):
64
+ self.close_serial()
65
+
66
+ @staticmethod
67
+ def auto_detect_com_port() -> Optional[str]:
68
+ com_ports = comports()
69
+ auto_com_port = None
70
+
71
+ for com_port in com_ports:
72
+ if com_port.vid == 0x454D and com_port.pid == 0x4E41:
73
+ auto_com_port = com_port.device
74
+ break
75
+
76
+ return auto_com_port
77
+
78
+ def write_data(self, data: bytearray):
79
+ LcdComm.write_data(self, data)
80
+
81
+ # Empty the input buffer after each write: we don't process acknowledgements the screen sends back
82
+ self.serial_flush_input()
83
+
84
+ def send_command(
85
+ self,
86
+ cmd: Command,
87
+ payload: Optional[bytearray] = None,
88
+ bypass_queue: bool = False,
89
+ ):
90
+ message = bytearray(cmd.value)
91
+
92
+ if payload:
93
+ message.extend(payload)
94
+
95
+ # If no queue for async requests, or if asked explicitly to do the request sequentially: do request now
96
+ if not self.update_queue or bypass_queue:
97
+ self.write_data(message)
98
+ else:
99
+ # Lock queue mutex then queue the request
100
+ with self.update_queue_mutex:
101
+ self.update_queue.put((self.write_data, [message]))
102
+
103
+ def initialize_comm(self):
104
+ pass
105
+
106
+ def reset(self):
107
+ # HW revision D does not implement a command to reset it: clear display instead
108
+ self.clear()
109
+
110
+ def clear(self):
111
+ # HW revision D does not implement a Clear command: display a blank image on the whole screen
112
+ color = 0xFFFF # RGB565 White color
113
+ color_bytes = bytearray(color.to_bytes(2, "big"))
114
+ self.send_command(cmd=Command.DISPCOLOR, payload=color_bytes)
115
+
116
+ def screen_off(self):
117
+ # HW revision D does not implement a "ScreenOff" native command: using SetBrightness(0) instead
118
+ self.set_brightness(0)
119
+
120
+ def screen_on(self):
121
+ # HW revision D does not implement a "ScreenOn" native command: using SetBrightness() instead
122
+ self.set_brightness()
123
+
124
+ def set_brightness(self, level: int = 25):
125
+ assert 0 <= level <= 100, "Brightness level must be [0-100]"
126
+
127
+ # Brightness scales from 0 to 500, with 500 being the brightest and 0 being the darkest.
128
+ # Convert our brightness % to an absolute value.
129
+ converted_level = level * 5
130
+
131
+ level_bytes = bytearray(converted_level.to_bytes(2, "big"))
132
+
133
+ # Send the command twice because sometimes it is not applied...
134
+ self.send_command(cmd=Command.SETBL, payload=level_bytes)
135
+ self.send_command(cmd=Command.SETBL, payload=level_bytes)
136
+
137
+ def set_orientation(self, orientation: Orientation = Orientation.PORTRAIT):
138
+ # In revision D, reverse orientations (reverse portrait / reverse landscape) are managed by the display
139
+ # Basic orientations (portrait / landscape) are software-managed because screen commands only support portrait
140
+ self.orientation = orientation
141
+
142
+ if (
143
+ self.orientation == Orientation.REVERSE_LANDSCAPE
144
+ or self.orientation == Orientation.REVERSE_PORTRAIT
145
+ ):
146
+ self.send_command(cmd=Command.SET180)
147
+ else:
148
+ self.send_command(cmd=Command.SETORG)
149
+
150
+ def paint(
151
+ self,
152
+ image: Image.Image,
153
+ pos: Tuple[int, int] = (0, 0),
154
+ ):
155
+ image = self._crop_to_display_bounds(image, pos)
156
+ image_width, image_height = image.size[0], image.size[1]
157
+
158
+ if image_height == 0 or image_width == 0:
159
+ return
160
+
161
+ x, y = pos
162
+ if (
163
+ self.orientation == Orientation.PORTRAIT
164
+ or self.orientation == Orientation.REVERSE_PORTRAIT
165
+ ):
166
+ (x0, y0) = (x, y)
167
+ (x1, y1) = (x + image_width - 1, y + image_height - 1)
168
+ else:
169
+ # Landscape / reverse landscape orientations are software managed: rotate image -90° and get new coordinates
170
+ image = image.rotate(270, expand=True)
171
+ (x0, y0) = (self.display_width - y - image_height, x)
172
+ (x1, y1) = (self.display_width - y - 1, x + image_width - 1)
173
+ image_width, image_height = image_height, image_width
174
+
175
+ # Send bitmap size
176
+ image_data = bytearray()
177
+ image_data += x0.to_bytes(2, "big")
178
+ image_data += x1.to_bytes(2, "big")
179
+ image_data += y0.to_bytes(2, "big")
180
+ image_data += y1.to_bytes(2, "big")
181
+ self.send_command(cmd=Command.BLOCKWRITE, payload=image_data)
182
+
183
+ # Prepare bitmap data transmission
184
+ self.send_command(Command.INTOPICMODE)
185
+
186
+ rgb565be = image_to_rgb565(image, "big")
187
+
188
+ # Lock queue mutex then queue all the requests for the image data
189
+ with self.update_queue_mutex:
190
+ for chunk in chunked(rgb565be, 63):
191
+ self.send_line(b"\x50" + chunk)
192
+
193
+ # Indicate the complete bitmap has been transmitted
194
+ self.send_command(Command.OUTPICMODE)
@@ -0,0 +1,168 @@
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 mimetypes
20
+ import shutil
21
+ from http.server import BaseHTTPRequestHandler, HTTPServer
22
+ from typing import Optional, Tuple
23
+ import queue
24
+ import threading
25
+ import logging
26
+
27
+ from PIL import Image
28
+
29
+ from .lcd_comm import LcdComm, Orientation
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ SCREENSHOT_FILE = "screencap.png"
34
+ WEBSERVER_PORT = 5678
35
+
36
+
37
+ # This webserver offer a blank page displaying simulated screen with auto-refresh
38
+ class SimulatedLcdWebServer(BaseHTTPRequestHandler):
39
+ def log_message(self, format, *args):
40
+ return
41
+
42
+ def do_GET(self): # noqa: N802
43
+ if self.path == "/":
44
+ self.send_response(200)
45
+ self.send_header("Content-type", "text/html")
46
+ self.end_headers()
47
+ self.wfile.write(
48
+ bytes('<img src="' + SCREENSHOT_FILE + '" id="myImage" />', "utf-8")
49
+ )
50
+ self.wfile.write(bytes("<script>", "utf-8"))
51
+ self.wfile.write(bytes("setInterval(function() {", "utf-8"))
52
+ self.wfile.write(
53
+ bytes(
54
+ " var myImageElement = document.getElementById('myImage');",
55
+ "utf-8",
56
+ )
57
+ )
58
+ self.wfile.write(
59
+ bytes(
60
+ " myImageElement.src = '"
61
+ + SCREENSHOT_FILE
62
+ + "?rand=' + Math.random();",
63
+ "utf-8",
64
+ )
65
+ )
66
+ self.wfile.write(bytes("}, 250);", "utf-8"))
67
+ self.wfile.write(bytes("</script>", "utf-8"))
68
+ elif self.path.startswith("/" + SCREENSHOT_FILE):
69
+ imgfile = open(SCREENSHOT_FILE, "rb").read()
70
+ mimetype = mimetypes.MimeTypes().guess_type(SCREENSHOT_FILE)[0]
71
+ self.send_response(200)
72
+ if mimetype is not None:
73
+ self.send_header("Content-type", mimetype)
74
+ self.end_headers()
75
+ self.wfile.write(imgfile)
76
+
77
+
78
+ # Simulated display: write on a file instead of serial port
79
+ class LcdSimulated(LcdComm):
80
+ def __init__(
81
+ self,
82
+ com_port: str = "AUTO",
83
+ display_width: int = 320,
84
+ display_height: int = 480,
85
+ update_queue: Optional[queue.Queue] = None,
86
+ ):
87
+ LcdComm.__init__(self, com_port, display_width, display_height, update_queue)
88
+ self.screen_image = Image.new(
89
+ "RGB", (self.width(), self.height()), (255, 255, 255)
90
+ )
91
+ self.screen_image.save("tmp", "PNG")
92
+ shutil.copyfile("tmp", SCREENSHOT_FILE)
93
+ self.orientation = Orientation.PORTRAIT
94
+
95
+ try:
96
+ self.webServer = HTTPServer(
97
+ ("localhost", WEBSERVER_PORT), SimulatedLcdWebServer
98
+ )
99
+ logger.debug(
100
+ "To see your simulated screen, open http://%s:%d in a browser"
101
+ % ("localhost", WEBSERVER_PORT)
102
+ )
103
+ threading.Thread(target=self.webServer.serve_forever).start()
104
+ except OSError:
105
+ logger.error(
106
+ "Error starting webserver! An instance might already be running on port %d."
107
+ % WEBSERVER_PORT
108
+ )
109
+
110
+ def __del__(self):
111
+ self.close_serial()
112
+
113
+ @staticmethod
114
+ def auto_detect_com_port() -> Optional[str]:
115
+ return None
116
+
117
+ def close_serial(self):
118
+ logger.debug("Shutting down web server")
119
+ self.webServer.shutdown()
120
+
121
+ def initialize_comm(self):
122
+ pass
123
+
124
+ def reset(self):
125
+ pass
126
+
127
+ def clear(self):
128
+ self.set_orientation(self.orientation)
129
+
130
+ def screen_off(self):
131
+ pass
132
+
133
+ def screen_on(self):
134
+ pass
135
+
136
+ def set_brightness(self, level: int = 25):
137
+ pass
138
+
139
+ def set_backplate_led_color(
140
+ self, led_color: Tuple[int, int, int] = (255, 255, 255)
141
+ ):
142
+ pass
143
+
144
+ def set_orientation(self, orientation: Orientation = Orientation.PORTRAIT):
145
+ self.orientation = orientation
146
+ # Just draw the screen again with the new width/height based on orientation
147
+ with self.update_queue_mutex:
148
+ self.screen_image = Image.new(
149
+ "RGB", (self.width(), self.height()), (255, 255, 255)
150
+ )
151
+ self.screen_image.save("tmp", "PNG")
152
+ shutil.copyfile("tmp", SCREENSHOT_FILE)
153
+
154
+ def paint(
155
+ self,
156
+ image: Image.Image,
157
+ pos: Tuple[int, int] = (0, 0),
158
+ ):
159
+ image = self._crop_to_display_bounds(image, pos)
160
+ image_width, image_height = image.size[0], image.size[1]
161
+
162
+ if image_height == 0 or image_width == 0:
163
+ return
164
+
165
+ with self.update_queue_mutex:
166
+ self.screen_image.paste(image, pos)
167
+ self.screen_image.save("tmp", "PNG")
168
+ shutil.copyfile("tmp", SCREENSHOT_FILE)
@@ -0,0 +1,58 @@
1
+ from typing import Iterator, Literal
2
+
3
+ import numpy as np
4
+ from PIL import Image
5
+
6
+
7
+ def chunked(data: bytes, chunk_size: int) -> Iterator[bytes]:
8
+ for i in range(0, len(data), chunk_size):
9
+ yield data[i : i + chunk_size]
10
+
11
+
12
+ def image_to_rgb565(image: Image.Image, endianness: Literal["big", "little"]) -> bytes:
13
+ if image.mode not in ["RGB", "RGBA"]:
14
+ # we need the first 3 channels to be R, G and B
15
+ image = image.convert("RGB")
16
+
17
+ rgb = np.asarray(image)
18
+
19
+ # flatten the first 2 dimensions (width and height) into a single stream
20
+ # of RGB pixels
21
+ rgb = rgb.reshape((image.size[1] * image.size[0], -1))
22
+
23
+ # extract R, G, B channels and promote them to 16 bits
24
+ r = rgb[:, 0].astype(np.uint16)
25
+ g = rgb[:, 1].astype(np.uint16)
26
+ b = rgb[:, 2].astype(np.uint16)
27
+
28
+ # construct RGB565
29
+ r = r >> 3
30
+ g = g >> 2
31
+ b = b >> 3
32
+ rgb565 = (r << 11) | (g << 5) | b
33
+
34
+ # serialize to the correct endianness
35
+ if endianness == "big":
36
+ typ = ">u2"
37
+ else:
38
+ typ = "<u2"
39
+ return rgb565.astype(typ).tobytes()
40
+
41
+
42
+ def image_to_bgr(image: Image.Image) -> bytes:
43
+ if image.mode not in ["RGB", "RGBA"]:
44
+ # we need the first 3 channels to be R, G and B
45
+ image = image.convert("RGB")
46
+ rgb = np.asarray(image)
47
+ # same as rgb[:, :, [2, 1, 0]] but faster
48
+ bgr = np.take(rgb, (2, 1, 0), axis=-1)
49
+ return bgr.tobytes()
50
+
51
+
52
+ def image_to_bgra(image: Image.Image) -> bytes:
53
+ if image.mode != "RGBA":
54
+ image = image.convert("RGBA")
55
+ rgba = np.asarray(image)
56
+ # same as rgba[:, :, [2, 1, 0, 3]] but faster
57
+ bgra = np.take(rgba, (2, 1, 0, 3), axis=-1)
58
+ return bgra.tobytes()
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: smartscreen-driver
3
+ Version: 0.2.0
4
+ Summary: Driver for serial-over-USB displays; library extracted from turing-smart-screen-python
5
+ Author: Mathieu Houdebine
6
+ Author-email: Hugo Chargois <hugo.chargois@free.fr>
7
+ License-File: LICENSE
8
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.9
12
+ Requires-Dist: numpy>=2.0.2
13
+ Requires-Dist: pillow>=11.1.0
14
+ Requires-Dist: pyserial>=3.5
15
+ Description-Content-Type: text/markdown
16
+
17
+ # smartscreen-driver
18
+
19
+ This package contains drivers for low-cost serial-over-USB displays such as
20
+ the Turing Smart Screen.
21
+
22
+ This library is simply an extract of the driver code from the
23
+ [turing-smart-screen-python](https://github.com/mathoudebine/turing-smart-screen-python)
24
+ project, removing all the sensors and UI code and dependencies, fixing coding
25
+ conventions violations (PEP8 et al) and adding proper Python packaging.
26
+
27
+ The usage is straightforward:
28
+
29
+ - you open the connection with the correct `LcdCommRevX` depending on your display
30
+ - you `paint()` (PIL) images to the display
31
+
32
+ See `hello_world.py` for an example.
@@ -0,0 +1,11 @@
1
+ smartscreen_driver/lcd_comm.py,sha256=AIkdLZfHFHE8I5TV1OocVvf4j8TBFlm_OBITOG4IQZ8,7608
2
+ smartscreen_driver/lcd_comm_rev_a.py,sha256=vnhd4UaWSVLhxXNBwsKi3f5jcIASBnj2gqQUnePgP7c,8271
3
+ smartscreen_driver/lcd_comm_rev_b.py,sha256=HJaDXKILtdHpuWiKGA5wa_u6q-zvuWhYrr1zcN4T43s,10896
4
+ smartscreen_driver/lcd_comm_rev_c.py,sha256=gxaAcX8IdTIq3V_kd_Bb3C10vSYxyguX0FyROsC6M_Q,13795
5
+ smartscreen_driver/lcd_comm_rev_d.py,sha256=16H_rNbYP66nebAePvZ897FIHjVjIeinLGm7-P2TP_w,7290
6
+ smartscreen_driver/lcd_simulated.py,sha256=ZI6-fUuFeps7_-0AyrQe2Ygo3dF3qfm8ougw0nNRFhE,5624
7
+ smartscreen_driver/serialize.py,sha256=xG_CBRutI8q3D3kTsG7dhLA0QYmQ_5LCZr9bfC4pey8,1671
8
+ smartscreen_driver-0.2.0.dist-info/METADATA,sha256=jlEFh2iP7lE2FXC_KJWOsqL5N2EUWEdOTZ6Sw5d-_HU,1203
9
+ smartscreen_driver-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ smartscreen_driver-0.2.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
11
+ smartscreen_driver-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any