diff --git a/library/lcd/lcd_comm.py b/library/lcd/lcd_comm.py index ef6ccc43..8214cb74 100644 --- a/library/lcd/lcd_comm.py +++ b/library/lcd/lcd_comm.py @@ -204,7 +204,7 @@ def DisplayBitmap(self, bitmap_path: str, x: int = 0, y: int = 0, width: int = 0 image = self.open_image(bitmap_path) self.DisplayPILImage(image, x, y, width, height) - def DisplayText( + def DrawText( self, text: str, x: int = 0, @@ -216,9 +216,7 @@ def DisplayText( background_image: str = None, align: str = 'left', anchor: str = None, - ): - # Convert text to bitmap using PIL and display it - # Provide the background image path to display text with transparent background + ) -> Tuple[Image.Image, int, int]: if isinstance(font_color, str): font_color = tuple(map(int, font_color.split(', '))) @@ -236,7 +234,7 @@ def DisplayText( if background_image is None: # A text bitmap is created with max width/height by default : text with solid background text_image = Image.new( - 'RGB', + 'RGBA', (self.get_width(), self.get_height()), background_color ) @@ -268,16 +266,34 @@ def DisplayText( # Crop text bitmap to keep only the text text_image = text_image.crop(box=(left, top, right, bottom)) + return (text_image, left, top) + + def DisplayText( + self, + text: str, + x: int = 0, + y: int = 0, + font: str = "roboto-mono/RobotoMono-Regular.ttf", + font_size: int = 20, + font_color: Tuple[int, int, int] = (0, 0, 0), + background_color: Tuple[int, int, int] = (255, 255, 255), + background_image: str = None, + align: str = 'left', + anchor: str = None, + ): + # Convert text to bitmap using PIL and display it + # Provide the background image path to display text with transparent background + text_image, left, top = self.DrawText(text, x, y, font, font_size, font_color, background_color, background_image, align, anchor) + self.DisplayPILImage(text_image, left, top) - def DisplayProgressBar(self, x: int, y: int, width: int, height: int, min_value: int = 0, max_value: int = 100, + def DrawProgressBar(self, x: int, y: int, width: int, height: int, min_value: int = 0, max_value: int = 100, value: int = 50, bar_color: Tuple[int, int, int] = (0, 0, 0), bar_outline: bool = True, background_color: Tuple[int, int, int] = (255, 255, 255), - background_image: str = None): - # Generate a progress bar and display it - # Provide the background image path to display progress bar with transparent background + background_image: str = None + ) -> Image: if isinstance(bar_color, str): bar_color = tuple(map(int, bar_color.split(', '))) @@ -300,7 +316,7 @@ def DisplayProgressBar(self, x: int, y: int, width: int, height: int, min_value: if background_image is None: # A bitmap is created with solid background - bar_image = Image.new('RGB', (width, height), background_color) + bar_image = Image.new('RGBA', (width, height), background_color) else: # A bitmap is created from provided background image bar_image = self.open_image(background_image) @@ -319,9 +335,23 @@ def DisplayProgressBar(self, x: int, y: int, width: int, height: int, min_value: # Draw outline draw.rectangle([0, 0, width - 1, height - 1], fill=None, outline=bar_color) - self.DisplayPILImage(bar_image, x, y) + return bar_image - def DisplayRadialProgressBar(self, xc: int, yc: int, radius: int, bar_width: int, + def DisplayProgressBar(self, x: int, y: int, width: int, height: int, min_value: int = 0, max_value: int = 100, + value: int = 50, + bar_color: Tuple[int, int, int] = (0, 0, 0), + bar_outline: bool = True, + background_color: Tuple[int, int, int] = (255, 255, 255), + background_image: str = None + ): + # Generate a progress bar and display it + # Provide the background image path to display progress bar with transparent background + progress_bar_image = self.DrawProgressBar(x, y, width, height, min_value, max_value, value, bar_color, bar_outline, background_color, background_image) + + self.DisplayPILImage(progress_bar_image, x, y) + + + def DrawRadialProgressBar(self, xc: int, yc: int, radius: int, bar_width: int, min_value: int = 0, max_value: int = 100, angle_start: int = 0, @@ -338,9 +368,6 @@ def DisplayRadialProgressBar(self, xc: int, yc: int, radius: int, bar_width: int bar_color: Tuple[int, int, int] = (0, 0, 0), background_color: Tuple[int, int, int] = (255, 255, 255), background_image: str = None): - # Generate a radial progress bar and display it - # Provide the background image path to display progress bar with transparent background - if isinstance(bar_color, str): bar_color = tuple(map(int, bar_color.split(', '))) @@ -378,7 +405,7 @@ def DisplayRadialProgressBar(self, xc: int, yc: int, radius: int, bar_width: int # if background_image is None: # A bitmap is created with solid background - bar_image = Image.new('RGB', (diameter, diameter), background_color) + bar_image = Image.new('RGBA', (diameter, diameter), background_color) else: # A bitmap is created from provided background image bar_image = self.open_image(background_image) @@ -472,6 +499,29 @@ def DisplayRadialProgressBar(self, xc: int, yc: int, radius: int, bar_width: int draw.text((radius - w / 2, radius - top - h / 2), text, font=font, fill=font_color) + return bar_image + + def DisplayRadialProgressBar(self, xc: int, yc: int, radius: int, bar_width: int, + min_value: int = 0, + max_value: int = 100, + angle_start: int = 0, + angle_end: int = 360, + angle_sep: int = 5, + angle_steps: int = 10, + clockwise: bool = True, + value: int = 50, + text: str = None, + with_text: bool = True, + font: str = "roboto/Roboto-Black.ttf", + font_size: int = 20, + font_color: Tuple[int, int, int] = (0, 0, 0), + bar_color: Tuple[int, int, int] = (0, 0, 0), + background_color: Tuple[int, int, int] = (255, 255, 255), + background_image: str = None): + # Generate a radial progress bar and display it + # Provide the background image path to display progress bar with transparent background + bar_image = self.DrawRadialProgressBar(xc, yc, radius, bar_width, min_value, max_value, angle_start, angle_end, angle_sep, angle_steps, clockwise, + value, text, with_text, font, font_size, font_color, bar_color, background_color, background_image) self.DisplayPILImage(bar_image, xc - radius, yc - radius) # Load image from the filesystem, or get from the cache if it has already been loaded previously diff --git a/library/lcd/lcd_comm_rev_c.py b/library/lcd/lcd_comm_rev_c.py index 48ef0c5e..0d564a74 100644 --- a/library/lcd/lcd_comm_rev_c.py +++ b/library/lcd/lcd_comm_rev_c.py @@ -22,6 +22,12 @@ import time from enum import Enum from math import ceil +import re +import struct +import os +import numpy as np +from typing import Tuple, Any +from numba import jit import serial from PIL import Image @@ -77,10 +83,21 @@ class Command(Enum): # STATIC IMAGE START_DISPLAY_BITMAP = bytearray((0x2c,)) PRE_UPDATE_BITMAP = bytearray((0x86, 0xef, 0x69, 0x00, 0x00, 0x00, 0x01)) - UPDATE_BITMAP = bytearray((0xcc, 0xef, 0x69, 0x00, 0x00)) + UPDATE_BITMAP = bytearray((0xcc, 0xef, 0x69, 0x00)) + + # VIDEO + START_VIDEO = bytearray((0x78, 0xef, 0x69, 0x00, 0x00, 0x00)) + INIT_VIDEO_OVERLAY = bytearray((0xd0, 0xef, 0x69, 0x00, 0x00, 0x00)) + + # FILES + LIST_FILES = bytearray((0x65, 0xef, 0x69, 0x00, 0x00, 0x00)) + UPLOAD_FILE = bytearray((0x6f, 0xef, 0x69, 0x00, 0x00, 0x00)) + DELETE_FILE = bytearray((0x66, 0xef, 0x69, 0x00, 0x00, 0x00)) + GET_FILE_SIZE = bytearray((0x6e, 0xef, 0x69, 0x00, 0x00, 0x00)) RESTARTSCREEN = bytearray((0x84, 0xef, 0x69, 0x00, 0x00, 0x00, 0x01)) DISPLAY_BITMAP = bytearray((0xc8, 0xef, 0x69, 0x00, 0x17, 0x70)) + DISPLAY_BITMAP_ON_VIDEO = bytearray((0xca, 0xef, 0x69, 0x00, 0x17, 0x70)) STARTMODE_DEFAULT = bytearray((0x00,)) STARTMODE_IMAGE = bytearray((0x01,)) @@ -136,6 +153,10 @@ def __init__(self, com_port: str = "AUTO", display_width: int = 480, display_hei logger.debug("HW revision: C") LcdComm.__init__(self, com_port, display_width, display_height, update_queue) self.openSerial() + # Video overlay is the image to be drawn on the video. + self.video_overlay = None + # Previous video overlay stores the image previously drawn on the video (so only the updated pixels can be sent). + self.previous_video_overlay = None def __del__(self): self.closeSerial() @@ -352,7 +373,7 @@ def _generate_update_image(self, image, x, y, count, cmd: Command = None, img_raw_data.append(f'{current_pixel[2]:02x}{current_pixel[1]:02x}{current_pixel[0]:02x}') image_msg = ''.join(img_raw_data) - image_size = f'{int((len(image_msg) / 2) + 2):04x}' # The +2 is for the "ef69" that will be added later. + image_size = f'{int((len(image_msg) / 2) + 2):06x}' # The +2 is for the "ef69" that will be added later. # logger.debug("Render Count: {}".format(count)) payload = bytearray() @@ -368,3 +389,331 @@ def _generate_update_image(self, image, x, y, count, cmd: Command = None, image_msg += 'ef69' return bytearray.fromhex(image_msg), payload + + def ListFiles(self, dir_path : str): + pyd = bytearray() + pyd.extend(len(dir_path).to_bytes(1)) + pyd.extend(Padding.NULL.value * 3) + pyd.extend(map(ord, dir_path)) + + self._send_command(Command.LIST_FILES, payload=pyd, bypass_queue=True) + # Read the reply (10240 bytes) + reply = self.ReadData(10240) + + reply = reply.strip(bytearray((0x0,))) + reply = reply.decode('ascii') + + # Reply format: result:dir:A/B/C/file:D/E/F/ + # Extract the list of subdirectories and the list of files from the reply. + + directories_match = re.findall('dir:(.*)file', reply) + directories = [] + if len(directories_match) > 0: + directories = directories_match[0].split('/') + directories.remove('') + + files_match = re.findall('file:(.*)', reply) + files = [] + if len(files_match) > 0: + files = files_match[0].split('/') + files.remove('') + + # Return the list of subdirectories and the list of files. + return directories, files + + def ListImagesInternalStorage(self): + return self.ListFiles("/root/img/") + + def ListVideosInternalStorage(self): + return self.ListFiles("/root/video/") + + def ListImagesSDStorage(self): + return self.ListFiles("/mnt/SDCARD/img/") + + def ListVideosSDStorage(self): + return self.ListFiles("/mnt/SDCARD/video/") + + def _read_in_chunks(self, file_object, chunk_size=249): + while True: + data = file_object.read(chunk_size) + if not data: + break + yield data + + def UploadFile(self, local_path : str, destination_path : str): + pyd = bytearray() + pyd.extend(len(destination_path).to_bytes(1)) + pyd.extend(Padding.NULL.value * 3) + pyd.extend(map(ord, destination_path)) + + file_size_bytes = os.path.getsize(local_path) + pyd.extend(struct.pack(' Image: + + update_array = np.zeros((self.get_height(), self.get_width(), 4), dtype=np.uint8) + previous_video_overlay_array = np.asarray(self.previous_video_overlay) + video_overlay_array = np.asarray(self.video_overlay) + + # Numpy to speed up the calculation. + # For each pixel, compare video overlay with previous video overlay and return + # an image with only the modified pixels. + diff_array = np.any(previous_video_overlay_array != video_overlay_array, axis=-1) + update_array[diff_array] = video_overlay_array[diff_array] + + update_image = Image.fromarray(update_array.astype('uint8'), 'RGBA') + return update_image + + + # Refresh the video overlay. + def ResfreshVideoOverlay(self): + + update_image = self._get_diff_image() + update_image_data = update_image.load() + video_overlay_data = self.video_overlay.load() + + update_image_array = np.asarray(update_image) + video_overlay_array = np.asarray(self.video_overlay) + + # Build image payload. + img_raw_data = [] + visible_pixels = [] + + for h in range(self.video_overlay.height): + + # Get the updated pixels segments for each screen line (so only the updated pixels are sent). + # updated_pixels_segments = LcdCommRevC._get_visible_segments(update_image_data, h, self.get_width()) + updated_pixels_segments = LcdCommRevC._get_visible_segments_numba(update_image_array, h, self.get_width()) + + # Get the visible segments for each screen line. + # visible_segments = LcdCommRevC._get_visible_segments(video_overlay_data, h, self.get_width()) + visible_segments = LcdCommRevC._get_visible_segments_numba(video_overlay_array, h, self.get_width()) + + # Send only updated pixels. + for segment in updated_pixels_segments: + x = segment[0] + segment_width = segment[1] + img_raw_data.append(f'{(h * self.display_height + x):06x}{segment_width:04x}') + + # Color. + for w in range(segment_width): + red, green, blue, alpha = video_overlay_data[x + w, h] + alpha_byte = int(alpha/255 * 15) + # color format (binary): b4 b3 b2 b1 0 0 a4 a3 | g4 g3 g2 g1 0 0 a2 a1 | r4 r3 r2 r1 0 0 0 0 + img_raw_data.append(f'{int(blue/255 * 15)<<4 | ((alpha_byte&0xC)>>2):02x}{int(green/255 * 15)<<4 | alpha_byte&0x3:02x}{int(red/255 * 15)<<4:02x}') + + # All visible pixels. + for segment in visible_segments: + x = segment[0] + segment_width = segment[1] + + # Set each segment as visible for the screen. + visible_pixels.append(f'{(h * self.display_height + x):06x}{segment_width:04x}') + + image_msg = ''.join(img_raw_data) + image_msg = image_msg + ''.join(visible_pixels) + + + visible_pixels_msg = ''.join(visible_pixels) + visible_pixels_size = int(len(visible_pixels_msg) / 2) + + if len(image_msg) > 500: + image_msg_temp = '00'.join(image_msg[i:i + 498] for i in range(0, len(image_msg), 498)) + + img_payload = bytearray.fromhex(image_msg_temp) + + # Fix image payload: last 250 bytes packet must not end with 0xef 0x69. + # and last 250 bytes packet must not be "0x69 0x0000..." nor "0xef 0x69 0x0000..." + if len(img_payload)>250 and (len(img_payload) % 250 == 0 or len(img_payload) % 250 == 248 or len(img_payload) % 250 == 249): + # Add a dummy "visible pixel" field to fix image payload format. + img_payload.extend(bytearray((0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0xef, 0x69))) + image_size = f'{int((len(image_msg) / 2) + 7):06x}' + visible_pixels_size = visible_pixels_size +5 + else: + img_payload.extend(bytearray((0xef, 0x69))) + image_size = f'{int((len(image_msg) / 2) + 2):06x}' + + # Build update image command. + update_image_payload = bytearray() + update_image_payload.extend(Command.UPDATE_BITMAP.value) + update_image_payload.extend(bytearray.fromhex(image_size)) + update_image_payload.extend(Padding.NULL.value * 3) + update_image_payload.extend(Count.Start.to_bytes(4, 'big')) + update_image_payload.extend(visible_pixels_size.to_bytes(4, 'big')) + + # Increment message ID counter. + Count.Start = Count.Start + 1 + self.previous_video_overlay = self.video_overlay.copy() + + self._send_command(Command.SEND_PAYLOAD, payload=update_image_payload) + self._send_command(Command.SEND_PAYLOAD, payload=img_payload) + + # Return the visible(eg non transparent) pixel segments from an image at a given line. + @staticmethod + def _get_visible_segments(image_data, y: int = 0, image_width : int = 800): + + visible_segments = [] + + i = 0 + j = 0 + while i < image_width: + # First non transparent pixel. + if image_data[i, y][3] > 0: + + # visible segment = position and length. + visible_segment = [i, 1] + j = i + 1 + while j < image_width and image_data[j, y][3] > 0: + visible_segment[1] = visible_segment[1] + 1 + j = j + 1 + + i = j + + visible_segments.append(visible_segment) + + i = i + 1 + + return visible_segments + + @jit(nopython=True, cache=True) + def _get_visible_segments_numba(image_data : np.ndarray, y: int = 0, image_width : int = 800): + + visible_segments = [] + + i = 0 + j = 0 + while i < image_width: + # First non transparent pixel. + if image_data[y, i][3] > 0: + + # visible segment = position and length. + visible_segment = [i, 1] + j = i + 1 + while j < image_width and (image_data[y, j][3] > 0 or image_data[y, j+1][3] > 0): + visible_segment[1] = visible_segment[1] + 1 + j = j + 1 + i = j + visible_segments.append(visible_segment) + + i = i + 1 + + return visible_segments + + def DrawPILImageOnVideo(self, image: Image, x: int = 0, y: int = 0): + # Paste image to draw on the video overlay image. + self.video_overlay.paste(image, (x, y)) + + def DrawTextOnVideo(self, text: str, x: int = 0, y: int = 0, + font: str = "roboto-mono/RobotoMono-Regular.ttf", font_size: int = 20, font_color: Tuple[int, int, int] = (255, 255, 255), + background_color: Any = None, + align: str = 'left', + anchor: str = None): + + # Convert text to bitmap and display it + (text_image, left, top) = self.DrawText(text, x, y, font, font_size, font_color, background_color, None, align, anchor) + + # Paste text image on the video overlay image. + self.video_overlay.paste(text_image, (left, top)) + + def DrawProgressBarOnVideo(self, x: int, y: int, width: int, height: int, min_value: int = 0, max_value: int = 100, + value: int = 50, + bar_color: Any = None, + bar_outline: bool = True, + background_color: Any = None): + + # Generate a progress bar and display it + progress_bar_image = self.DrawProgressBar(x, y, width, height, min_value, max_value, value, bar_color, bar_outline, background_color, None) + + self.video_overlay.paste(progress_bar_image, (x, y)) + + def DrawRadialProgressBarOnVideo(self, xc: int, yc: int, radius: int, bar_width: int, + min_value: int = 0, + max_value: int = 100, + angle_start: int = 0, + angle_end: int = 360, + angle_sep: int = 5, + angle_steps: int = 10, + clockwise: bool = True, + value: int = 50, + text: str = None, + with_text: bool = True, + font: str = "roboto/Roboto-Black.ttf", + font_size: int = 20, + font_color: Any = None, + bar_color: Any = None, + background_color: Any = None): + + # Generate a radial progress bar and display it + bar_image = self.DrawRadialProgressBar(xc, yc, radius, bar_width, min_value, max_value, angle_start, angle_end, angle_sep, angle_steps, clockwise, + value, text, with_text, font, font_size, font_color, bar_color, background_color, None) + + self.video_overlay.paste(bar_image, ( xc - radius, yc - radius)) diff --git a/res/videos/ethereal_wave.mp4 b/res/videos/ethereal_wave.mp4 new file mode 100644 index 00000000..5a751579 Binary files /dev/null and b/res/videos/ethereal_wave.mp4 differ diff --git a/res/videos/honeycome_white.mp4 b/res/videos/honeycome_white.mp4 new file mode 100644 index 00000000..f4842558 Binary files /dev/null and b/res/videos/honeycome_white.mp4 differ diff --git a/res/videos/license.txt b/res/videos/license.txt new file mode 100644 index 00000000..fd22005a --- /dev/null +++ b/res/videos/license.txt @@ -0,0 +1,11 @@ +License: + +Free Stock video by Videezy + +https://www.videezy.com/backgrounds/5089-particle-wave-4k-motion-background-loop +https://www.videezy.com/backgrounds/5087-organic-lines-4k-motion-background-loop +https://www.videezy.com/backgrounds/5067-ethereal-wave-4k-motion-background-loop +https://www.videezy.com/backgrounds/5106-tri-particles-4k-motion-background-loop +https://www.videezy.com/abstract/41894-abstract-light-neon-frame +https://www.videezy.com/abstract/41618-abstract-honeycomb-background +https://www.videezy.com/technology/50766-matrix-digital-background \ No newline at end of file diff --git a/res/videos/matrix.mp4 b/res/videos/matrix.mp4 new file mode 100644 index 00000000..0e40a42a Binary files /dev/null and b/res/videos/matrix.mp4 differ diff --git a/res/videos/neon_frame.mp4 b/res/videos/neon_frame.mp4 new file mode 100644 index 00000000..320c491e Binary files /dev/null and b/res/videos/neon_frame.mp4 differ diff --git a/res/videos/organic_lines.mp4 b/res/videos/organic_lines.mp4 new file mode 100644 index 00000000..64de6bda Binary files /dev/null and b/res/videos/organic_lines.mp4 differ diff --git a/res/videos/particle_wave.mp4 b/res/videos/particle_wave.mp4 new file mode 100644 index 00000000..1536c39d Binary files /dev/null and b/res/videos/particle_wave.mp4 differ diff --git a/res/videos/tri_particles.mp4 b/res/videos/tri_particles.mp4 new file mode 100644 index 00000000..e743f1b5 Binary files /dev/null and b/res/videos/tri_particles.mp4 differ diff --git a/simple-program-video-mode.py b/simple-program-video-mode.py new file mode 100644 index 00000000..703e73df --- /dev/null +++ b/simple-program-video-mode.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python +# turing-smart-screen-python - a Python system monitor and library for USB-C displays like Turing Smart Screen or XuanFang +# https://github.com/mathoudebine/turing-smart-screen-python/ + +# Copyright (C) 2021-2023 Matthieu Houdebine (mathoudebine) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# This file is a simple Python test program using the library code to display custom content on screen (see README) + +import os +import signal +import sys +import time +from datetime import datetime + +# Import only the modules for LCD communication +from library.lcd.lcd_comm_rev_a import LcdCommRevA, Orientation +from library.lcd.lcd_comm_rev_b import LcdCommRevB +from library.lcd.lcd_comm_rev_c import LcdCommRevC +from library.lcd.lcd_comm_rev_d import LcdCommRevD +from library.lcd.lcd_simulated import LcdSimulated +from library.log import logger + +# Set your COM port e.g. COM3 for Windows, /dev/ttyACM0 for Linux, etc. or "AUTO" for auto-discovery +# COM_PORT = "/dev/ttyACM0" +# COM_PORT = "COM5" +COM_PORT = "COM4" + +# Display revision: +# - A for Turing 3.5" and UsbPCMonitor 3.5"/5" +# - B for Xuanfang 3.5" (inc. flagship) +# - C for Turing 5" +# - D for Kipye Qiye Smart Display 3.5" +# - SIMU for 3.5" simulated LCD (image written in screencap.png) +# - SIMU5 for 5" simulated LCD +# To identify your smart screen: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions +REVISION = "C" + +stop = False + +if __name__ == "__main__": + + def sighandler(signum, frame): + global stop + stop = True + + + # Set the signal handlers, to send a complete frame to the LCD before exit + signal.signal(signal.SIGINT, sighandler) + signal.signal(signal.SIGTERM, sighandler) + is_posix = os.name == 'posix' + if is_posix: + signal.signal(signal.SIGQUIT, sighandler) + + # Build your LcdComm object for Turing 5" + logger.info("Selected Hardware Revision C (Turing Smart Screen 5\")") + lcd_comm = LcdCommRevC(com_port=COM_PORT) + + # Set LANDSCAPE orientation. + orientation = Orientation.LANDSCAPE + lcd_comm.SetOrientation(orientation=orientation) + + # Send initialization commands + lcd_comm.InitializeComm() + + # Set brightness in % (warning: revision A display can get hot at high brightness! Keep value at 50% max for rev. A) + lcd_comm.SetBrightness(level=50) + + # Check if video is loaded and upload video if needed. + video_size = lcd_comm.GetFileSize("/mnt/SDCARD/video/particle_wave.mp4") + + if video_size == 0: + # Upload a background video. + print("Uploading video ...") + lcd_comm.UploadFile(local_path="res/videos/particle_wave.mp4", destination_path="/mnt/SDCARD/video/particle_wave.mp4") + print("Upload done") + + # Print size of the uploaded video. + print(lcd_comm.GetFileSize("/mnt/SDCARD/video/particle_wave.mp4")) + + # Clear screen. + lcd_comm.StopVideo() + lcd_comm.Clear() + + # Start the backgroud video. + lcd_comm.StartVideo("/mnt/SDCARD/video/particle_wave.mp4") + + # Initialize the video overlay. + # Must be called before drawing anything on the video! + lcd_comm.InitializeVideoOverlay() + + # Display sample text + lcd_comm.DrawTextOnVideo("Basic text", 50, 85) + + # Display custom text with solid background + lcd_comm.DrawTextOnVideo("Custom italic multiline text\nright-aligned", 5, 120, + font="roboto/Roboto-Italic.ttf", + font_size=20, + font_color=(0, 0, 255), + background_color=(255, 255, 0), + align='right') + + # Display custom text with transparent background + lcd_comm.DrawTextOnVideo("Transparent bold text", 5, 180, + font="geforce/GeForce-Bold.ttf", + font_size=30, + font_color=(255, 255, 255)) + + lcd_comm.ResfreshVideoOverlay() + + # Display the current time and some progress bars as fast as possible + bar_value = 0 + while not stop: + start = time.perf_counter() + lcd_comm.DrawTextOnVideo(str(datetime.now().time()), 160, 2, + font="roboto/Roboto-Bold.ttf", + font_size=20, + font_color=(255, 0, 0)) + + lcd_comm.DrawProgressBarOnVideo(10, 40, + width=140, height=30, + min_value=0, max_value=100, value=bar_value, + bar_color=(255, 255, 0), bar_outline=True) + + lcd_comm.DrawProgressBarOnVideo(160, 40, + width=140, height=30, + min_value=0, max_value=19, value=bar_value % 20, + bar_color=(0, 255, 0), bar_outline=False) + + lcd_comm.DrawRadialProgressBarOnVideo(98, 260, 25, 4, + min_value=0, + max_value=100, + value=bar_value, + angle_sep=0, + bar_color=(0, 255, 0), + font_color=(255, 255, 255)) + + lcd_comm.DrawRadialProgressBarOnVideo(222, 260, 40, 13, + min_value=0, + max_value=100, + angle_start=405, + angle_end=135, + angle_steps=10, + angle_sep=5, + clockwise=False, + value=bar_value, + bar_color=(255, 255, 0), + text=f"{10 * int(bar_value / 10)}°C", + font="geforce/GeForce-Bold.ttf", + font_size=20, + font_color=(255, 255, 0)) + + lcd_comm.ResfreshVideoOverlay() + + bar_value = (bar_value + 2) % 101 + end = time.perf_counter() + logger.debug(f"refresh done (took {end - start:.3f} s)") + + # Close serial connection at exit + lcd_comm.closeSerial() diff --git a/simple-program.py b/simple-program.py index ce5d0618..7a7a071b 100755 --- a/simple-program.py +++ b/simple-program.py @@ -36,7 +36,7 @@ # Set your COM port e.g. COM3 for Windows, /dev/ttyACM0 for Linux, etc. or "AUTO" for auto-discovery # COM_PORT = "/dev/ttyACM0" # COM_PORT = "COM5" -COM_PORT = "AUTO" +COM_PORT = "COM4" # Display revision: # - A for Turing 3.5" and UsbPCMonitor 3.5"/5" @@ -46,7 +46,7 @@ # - SIMU for 3.5" simulated LCD (image written in screencap.png) # - SIMU5 for 5" simulated LCD # To identify your smart screen: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions -REVISION = "A" +REVISION = "C" stop = False diff --git a/test_new_features.py b/test_new_features.py new file mode 100644 index 00000000..5e5da2dd --- /dev/null +++ b/test_new_features.py @@ -0,0 +1,114 @@ +# This file is a simple Python test program using the library code to demonstrate the new video features + +import os +import signal +import sys +import time +from datetime import datetime +from PIL import Image + +# Import only the modules for LCD communication +from library.lcd.lcd_comm_rev_a import LcdCommRevA, Orientation +from library.lcd.lcd_comm_rev_b import LcdCommRevB +from library.lcd.lcd_comm_rev_c import LcdCommRevC, Command, Padding +from library.lcd.lcd_simulated import LcdSimulated +from library.log import logger + +# Set your COM port e.g. COM3 for Windows, /dev/ttyACM0 for Linux, etc. or "AUTO" for auto-discovery +# COM_PORT = "/dev/ttyACM0" +COM_PORT = "COM4" + +# Display revision: A for Turing 3.5", B for Xuanfang 3.5" (inc. flagship), C for Turing 5" +# Use SIMU for 3.5" simulated LCD (image written in screencap.png) or SIMU5 for 5" simulated LCD +# To identify your revision: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions + +stop = False + +if __name__ == "__main__": + + def sighandler(signum, frame): + global stop + stop = True + + # Set the signal handlers, to send a complete frame to the LCD before exit + signal.signal(signal.SIGINT, sighandler) + signal.signal(signal.SIGTERM, sighandler) + is_posix = os.name == 'posix' + if is_posix: + signal.signal(signal.SIGQUIT, sighandler) + + # Build your LcdComm object for Turing 5" + print("Selected Hardware Revision C (Turing Smart Screen 5\")") + lcd_comm = LcdCommRevC(com_port=COM_PORT, + display_width=480, + display_height=800) + + # Set LANDSCAPE orientation. + orientation = Orientation.LANDSCAPE + lcd_comm.SetOrientation(orientation=orientation) + + # Send initialization commands + lcd_comm.InitializeComm() + + ##### New feature examples ##### + + # List files in root directory. + print(lcd_comm.ListFiles("/")) + + # List: internal images ; internal videos ; SDCARD images ; SDCARD videos. + print(lcd_comm.ListImagesInternalStorage()) + print(lcd_comm.ListVideosInternalStorage()) + print(lcd_comm.ListImagesSDStorage()) + print(lcd_comm.ListVideosSDStorage()) + + # Check if video is loaded and upload video if needed. + video_size = lcd_comm.GetFileSize("/mnt/SDCARD/video/particle_wave.mp4") + + if video_size == 0: + # Upload a background video. + print("Uploading video ...") + lcd_comm.UploadFile(local_path="res/videos/particle_wave.mp4", destination_path="/mnt/SDCARD/video/particle_wave.mp4") + print("Upload done") + + # Print size of the uploaded video. + print(lcd_comm.GetFileSize("/mnt/SDCARD/video/particle_wave.mp4")) + + # Clear screen. + lcd_comm.StopVideo() + lcd_comm.Clear() + + # Start the backgroud video. + lcd_comm.StartVideo("/mnt/SDCARD/video/particle_wave.mp4") + + # Initialize the video overlay. + # Must be called before drawing anything on the video! + lcd_comm.InitializeVideoOverlay() + + # Draw a red square. + test_image = Image.new("RGB", (50, 50), (255, 0, 0)) + lcd_comm.DrawPILImageOnVideo(test_image, x=10, y=10) + + # Refresh n°1 + time.sleep(1) + lcd_comm.ResfreshVideoOverlay() + + # Transparent gray square. + test_image2 = Image.new("RGBA", (100, 100), (64, 64, 64, 64)) + lcd_comm.DrawPILImageOnVideo(test_image2, x=250, y=50) + + # Yellow square. + test_image3 = Image.new("RGB", (20, 20), (255, 255, 0)) + lcd_comm.DrawPILImageOnVideo(test_image3, x=400, y=60) + + # Refresh n°2 + time.sleep(1) + lcd_comm.ResfreshVideoOverlay() + + # Display sample text on video. + lcd_comm.DrawTextOnVideo("Basic text", 50, 100) + + # Refresh n°3 + lcd_comm.ResfreshVideoOverlay() + + # Close serial connection at exit + lcd_comm.closeSerial()