Skip to content

Commit 2a3effc

Browse files
committed
Rename files, fix issue with rotation for USB devices
1 parent 06e882d commit 2a3effc

File tree

3 files changed

+221
-12
lines changed

3 files changed

+221
-12
lines changed

library/display.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from library.lcd.lcd_comm_rev_a import LcdCommRevA
2424
from library.lcd.lcd_comm_rev_b import LcdCommRevB
2525
from library.lcd.lcd_comm_rev_c import LcdCommRevC
26-
from library.lcd.lcd_comm_rev_c_usb import LcdCommRevCUSB
26+
from library.lcd.lcd_comm_turing_usb import LcdCommTuringUSB
2727
from library.lcd.lcd_comm_rev_d import LcdCommRevD
2828
from library.lcd.lcd_comm_weact_a import LcdCommWeActA
2929
from library.lcd.lcd_comm_weact_b import LcdCommWeActB
@@ -88,7 +88,7 @@ def __init__(self):
8888
update_queue=config.update_queue, display_width=width, display_height=height)
8989
elif config.CONFIG_DATA["display"]["REVISION"] == "C_USB":
9090
# On all USB models, manually configure screen width/height from theme
91-
self.lcd = LcdCommRevCUSB(display_width=width, display_height=height)
91+
self.lcd = LcdCommTuringUSB(display_width=width, display_height=height)
9292
elif config.CONFIG_DATA["display"]["REVISION"] == "D":
9393
self.lcd = LcdCommRevD(com_port=config.CONFIG_DATA['config']['COM_PORT'],
9494
update_queue=config.update_queue)

library/lcd/lcd_comm.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_hei
5050
self.lcd_serial = None
5151

5252
# String containing absolute path to serial port e.g. "COM3", "/dev/ttyACM1" or "AUTO" for auto-discovery
53+
# Ignored for USB HID screens
5354
self.com_port = com_port
5455

5556
# Display always start in portrait orientation by default
@@ -180,8 +181,8 @@ def ReadData(self, readSize: int):
180181
return self.serial_read(readSize)
181182

182183
@staticmethod
183-
@abstractmethod
184184
def auto_detect_com_port() -> Optional[str]:
185+
# To implement only for screens that use serial commands
185186
pass
186187

187188
@abstractmethod

library/lcd/lcd_comm_rev_c_usb.py renamed to library/lcd/lcd_comm_turing_usb.py

Lines changed: 217 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
# SPDX-License-Identifier: GPL-3.0-or-later
2+
#
3+
# turing-smart-screen-python - a Python system monitor and library for USB-C displays like Turing Smart Screen or XuanFang
4+
# https://github.com/mathoudebine/turing-smart-screen-python/
5+
#
6+
# Copyright (C) 2021 Matthieu Houdebine (mathoudebine)
7+
#
8+
# This program is free software: you can redistribute it and/or modify
9+
# it under the terms of the GNU General Public License as published by
10+
# the Free Software Foundation, either version 3 of the License, or
11+
# (at your option) any later version.
12+
#
13+
# This program is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU General Public License
19+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
21+
import math
122
import platform
223
import queue
324
import struct
@@ -12,12 +33,16 @@
1233
from Crypto.Cipher import DES
1334
from PIL import Image
1435

36+
from library.log import logger
1537
from library.lcd.lcd_comm import Orientation, LcdComm
1638

1739
VENDOR_ID = 0x1cbe
1840
PRODUCT_ID = 0x0088
1941

2042

43+
MAX_CHUNK_BYTES = 1024*1024 # Data sent to screen cannot exceed 1024MB or there will be a timeout
44+
45+
2146
def build_command_packet_header(a0: int) -> bytearray:
2247
packet = bytearray(500)
2348
packet[0] = a0
@@ -288,17 +313,185 @@ def send_video(dev, video_path, loop=False):
288313
write_to_device(dev, encrypt_command_packet(build_command_packet_header(123)))
289314

290315

316+
def _encode_png(image: Image.Image) -> bytes:
317+
buffer = BytesIO()
318+
image.save(buffer, format="PNG", compress_level=9)
319+
return buffer.getvalue()
320+
321+
322+
def compress_image(image: Image.Image, ratio: float) -> Image.Image:
323+
width, height = image.size
324+
image = image.resize((int(width * ratio*0.5), int(height * ratio*0.5)),
325+
resample=Image.Resampling.LANCZOS)
326+
image = image.resize((width, height))
327+
return image
328+
329+
330+
331+
def upload_file(dev, file_path: str) -> bool:
332+
local_path = Path(file_path)
333+
if not local_path.exists():
334+
logger.error("Error: File does not exist: %s", file_path)
335+
return False
336+
337+
ext = local_path.suffix.lower()
338+
if ext == ".png":
339+
device_path = f"/tmp/sdcard/mmcblk0p1/img/{local_path.name}"
340+
logger.info("Uploading PNG: %s → %s", file_path, device_path)
341+
elif ext == ".mp4":
342+
h264_path = extract_h264_from_mp4(file_path)
343+
device_path = f"/tmp/sdcard/mmcblk0p1/video/{h264_path.name}"
344+
local_path = h264_path # Update local path to .h264
345+
logger.info("Uploading MP4 as H264: %s → %s", local_path, device_path)
346+
else:
347+
logger.error("Error: Unsupported file type. Only .png and .mp4 are allowed.")
348+
return False
349+
350+
if not _open_file_command(dev, device_path):
351+
logger.error("Failed to open remote file for writing.")
352+
return False
353+
354+
if not _write_file_command(dev, str(local_path)):
355+
logger.error("Failed to write file data.")
356+
return False
357+
358+
logger.info("Upload completed successfully.")
359+
return True
360+
361+
362+
def _open_file_command(dev, path: str):
363+
logger.info("Opening remote file: %s", path)
364+
365+
path_bytes = path.encode("ascii")
366+
length = len(path_bytes)
367+
368+
packet = build_command_packet_header(38)
369+
370+
packet[8] = (length >> 24) & 0xFF
371+
packet[9] = (length >> 16) & 0xFF
372+
packet[10] = (length >> 8) & 0xFF
373+
packet[11] = length & 0xFF
374+
packet[12:16] = b"\x00\x00\x00\x00"
375+
packet[16 : 16 + length] = path_bytes
376+
377+
return write_to_device(dev, encrypt_command_packet(packet))
378+
379+
380+
def _delete_command(dev, file_path: str):
381+
logger.info("Deleting remote file: %s", file_path)
382+
383+
path_bytes = file_path.encode("ascii")
384+
length = len(path_bytes)
385+
386+
packet = build_command_packet_header(40)
387+
packet[8] = (length >> 24) & 0xFF
388+
packet[9] = (length >> 16) & 0xFF
389+
packet[10] = (length >> 8) & 0xFF
390+
packet[11] = length & 0xFF
391+
packet[12:16] = b"\x00\x00\x00\x00"
392+
packet[16 : 16 + length] = path_bytes
393+
394+
return write_to_device(dev, encrypt_command_packet(packet))
395+
396+
397+
def _play_command(dev, file_path: str):
398+
logger.info("Requesting playback for: %s", file_path)
399+
400+
path_bytes = file_path.encode("ascii")
401+
length = len(path_bytes)
402+
403+
packet = build_command_packet_header(98)
404+
405+
packet[8] = (length >> 24) & 0xFF
406+
packet[9] = (length >> 16) & 0xFF
407+
packet[10] = (length >> 8) & 0xFF
408+
packet[11] = length & 0xFF
409+
packet[12:16] = b"\x00\x00\x00\x00"
410+
packet[16 : 16 + length] = path_bytes
411+
412+
return write_to_device(dev, encrypt_command_packet(packet))
413+
414+
415+
def _play2_command(dev, file_path: str):
416+
logger.info("Requesting alternate playback for: %s", file_path)
417+
418+
path_bytes = file_path.encode("ascii")
419+
length = len(path_bytes)
420+
421+
packet = build_command_packet_header(110)
422+
423+
packet[8] = (length >> 24) & 0xFF
424+
packet[9] = (length >> 16) & 0xFF
425+
packet[10] = (length >> 8) & 0xFF
426+
packet[11] = length & 0xFF
427+
packet[12:16] = b"\x00\x00\x00\x00"
428+
packet[16 : 16 + length] = path_bytes
429+
430+
return write_to_device(dev, encrypt_command_packet(packet))
431+
432+
433+
def _play3_command(dev, file_path: str):
434+
logger.info("Requesting image playback for: %s", file_path)
435+
436+
path_bytes = file_path.encode("ascii")
437+
length = len(path_bytes)
438+
439+
packet = build_command_packet_header(113)
440+
441+
packet[8] = (length >> 24) & 0xFF
442+
packet[9] = (length >> 16) & 0xFF
443+
packet[10] = (length >> 8) & 0xFF
444+
packet[11] = length & 0xFF
445+
packet[12:16] = b"\x00\x00\x00\x00"
446+
packet[16 : 16 + length] = path_bytes
447+
448+
return write_to_device(dev, encrypt_command_packet(packet))
449+
450+
451+
def _write_file_command(dev, file_path: str) -> bool:
452+
logger.info("Writing remote file from: %s", file_path)
453+
454+
try:
455+
with open(file_path, "rb") as fh:
456+
chunk_index = 0
457+
while True:
458+
data_chunk = fh.read(202752)
459+
if not data_chunk:
460+
break
461+
462+
chunk_size = len(data_chunk)
463+
chunk_index += 1
464+
logger.debug("Chunk %d size: %d bytes", chunk_index, chunk_size)
465+
466+
cmd_packet = build_command_packet_header(39)
467+
cmd_packet[8] = (chunk_size >> 24) & 0xFF
468+
cmd_packet[9] = (chunk_size >> 16) & 0xFF
469+
cmd_packet[10] = (chunk_size >> 8) & 0xFF
470+
cmd_packet[11] = chunk_size & 0xFF
471+
472+
response = write_to_device(dev, encrypt_command_packet(cmd_packet) + data_chunk)
473+
if response is None:
474+
logger.error("Write command failed at chunk %d", chunk_index)
475+
return False
476+
477+
logger.info("File write completed successfully (%d chunks).", chunk_index)
478+
return True
479+
except FileNotFoundError:
480+
logger.error("File not found: %s", file_path)
481+
return False
482+
except Exception as exc:
483+
logger.error("Error writing file: %s", exc)
484+
return False
485+
291486
# This class is for Turing Smart Screen newer models (5.2" / 8" / 8.8" HW rev 1.x / 9.2")
292-
class LcdCommRevCUSB(LcdComm):
487+
# These models are not detected as serial ports but as (Win)USB devices
488+
class LcdCommTuringUSB(LcdComm):
293489
def __init__(self, com_port: str = "AUTO", display_width: int = 480, display_height: int = 1920,
294490
update_queue: Optional[queue.Queue] = None):
295491
super().__init__(com_port, display_width, display_height, update_queue)
296492
self.dev = find_usb_device()
297493
# Store the current screen state as an image that will be continuously updated and sent
298-
self.current_state = Image.new("RGBA", (self.get_width(), self.get_height()), (0, 0, 0, 255))
299-
300-
def auto_detect_com_port(self):
301-
pass
494+
self.current_state = Image.new("RGBA", (self.get_width(), self.get_height()), (0, 0, 0, 0))
302495

303496
def InitializeComm(self):
304497
send_sync_command(self.dev)
@@ -327,6 +520,8 @@ def SetBrightness(self, level: int = 25):
327520

328521
def SetOrientation(self, orientation: Orientation):
329522
self.orientation = orientation
523+
# Recreate new state with correct width/height now that screen orientation has changed
524+
self.current_state = Image.new("RGBA", (self.get_width(), self.get_height()), (0, 0, 0, 0))
330525

331526
def DisplayPILImage(self, image: Image.Image, x: int = 0, y: int = 0, image_width: int = 0, image_height: int = 0):
332527
if not image_height:
@@ -355,9 +550,22 @@ def DisplayPILImage(self, image: Image.Image, x: int = 0, y: int = 0, image_widt
355550
else: # Orientation.REVERSE_PORTRAIT is initial screen orientation
356551
base_image = self.current_state
357552

358-
# Save as PNG format with headers
359-
buffer = BytesIO()
360-
base_image.save(buffer, format="PNG")
553+
# total_size = len(_encode_png(base_image))
554+
# print("total size =", total_size/1024)
555+
#
556+
# if total_size > 1024*1024:
557+
#
558+
# # If bitmap is > 1024MB operation will timeout: compress it
559+
# size_overflow = total_size - 1024*1024
560+
# ratio = 1- (size_overflow / total_size)
561+
# print("ratio = ", ratio)
562+
#
563+
# base_image = compress_image(base_image, ratio)
564+
#
565+
# new_size = len(_encode_png(base_image))
566+
# print("new_size =", new_size/1024)
567+
361568

362569
# Send PNG data
363-
send_image(self.dev, buffer.getvalue())
570+
encoded = _encode_png(base_image)
571+
send_image(self.dev, encoded)

0 commit comments

Comments
 (0)