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,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