diff --git a/README.md b/README.md index cec26118..2c6400bb 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,30 @@ # ![Icon](https://raw.githubusercontent.com/mathoudebine/turing-smart-screen-python/main/res/icons/monitor-icon-17865/24.png) turing-smart-screen-python -### ⚠️ DISCLAIMER - PLEASE READ ⚠️ - -This project is **not affiliated, associated, authorized, endorsed by, or in any way officially connected with Turing / XuanFang / Kipye brands**, or any of theirs subsidiaries, affiliates, manufacturers or sellers of their products. All product and company names are the registered trademarks of their original owners. - -This project is an open-source alternative software, NOT the original software provided for the smart screens. **Please do not open issues for USBMonitor.exe/ExtendScreen.exe or for the smart screens hardware here**. -* for Turing Smart Screen, use the official forum here: http://discuz.turzx.com/ -* for other smart screens, contact your reseller ---- +> [!WARNING] +> +> This project is **not affiliated, associated, authorized, endorsed by, or in any way officially connected with Turing / XuanFang / Kipye brands**, or any of theirs subsidiaries, affiliates, manufacturers or sellers of their products. All product and company names are the registered trademarks of their original owners. +> +> This project is an open-source alternative software, NOT the original software provided for the smart screens. **Please do not open issues for USBMonitor.exe/ExtendScreen.exe or for the smart screens hardware here**. +> * for Turing Smart Screen, use the official forum here: http://discuz.turzx.com/ +> * for other smart screens, contact your reseller ![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black) ![Windows](https://img.shields.io/badge/Windows%2010%2F11-0078D6?style=for-the-badge&logoColor=white&logo=data:image/svg%2bxml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+V2luZG93czwvdGl0bGU+PHBhdGggZmlsbCA9ICIjRkZGRkZGIiBkPSJNMCwwSDExLjM3N1YxMS4zNzJIMFpNMTIuNjIzLDBIMjRWMTEuMzcySDEyLjYyM1pNMCwxMi42MjNIMTEuMzc3VjI0SDBabTEyLjYyMywwSDI0VjI0SDEyLjYyMyIvPjwvc3ZnPg==) [![macOS](https://img.shields.io/badge/mac%20os%20(⚠️major%20bug)-000000?style=for-the-badge&logo=apple&logoColor=white)](https://github.com/mathoudebine/turing-smart-screen-python/issues/7) ![Raspberry Pi](https://img.shields.io/badge/Raspberry%20Pi-A22846?style=for-the-badge&logo=Raspberry%20Pi&logoColor=white) ![Python](https://img.shields.io/badge/Python-3.X-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54) [![Licence](https://img.shields.io/github/license/mathoudebine/turing-smart-screen-python?style=for-the-badge)](./LICENSE) -A Python system monitor program and an abstraction library for **small IPS USB-C (UART) displays.** +A Python system monitor program and an abstraction library for **small IPS USB-C displays.** Supported operating systems : macOS, Windows, Linux (incl. Raspberry Pi), basically all OS that support Python 3.9+ ### ✅ Supported smart screens models: -| ✅ Turing Smart Screen 3.5" | ✅ XuanFang 3.5" | ✅ Turing Smart Screen 5" | -|------------------------------------------------------|---------------------------------------------------|---------------------------------------------| -| | | | -| also improperly called "revision A" by the resellers | revision B & flagship (with backplate & RGB LEDs) | basic support (no video or storage for now) | - -| ⚠️ Turing Smart Screen 8.8" | ✅ Turing Smart Screen 2.1" / 2.8" | -|---------------------------------------------|------------------------------------------------------------------| -| | | -| basic support (no video or storage for now)
⚠️ [New revision V1.1 not supported!](https://github.com/mathoudebine/turing-smart-screen-python/issues/727) | basic support (no video or storage for now) | +| ✅ Turing Smart Screen / TURZX | +|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|
| +| All available sizes and hardware revisions supported: **2.1" / 2.8" / 3.5" / 4.6" / 5" / 5.2" / 8.0" / 8.8" / 9.2" / 12.3"**
UART and WinUSB protocols supported. Note: no video or storage support for now | -| ✅ [UsbPCMonitor 3.5" / 5"](https://aliexpress.com/item/1005003931363455.html) | ✅ [Kipye Qiye Smart Display 3.5"](https://www.aliexpress.us/item/3256803899049957.html) | -|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| -| | | -| Unknown manufacturer, visually similar to Turing 3.5" / 5". Original software is `UsbPCMonitor.exe` | Front panel has an engraved inscription "奇叶智显" Qiye Zhixian (Qiye Smart Display) | +| ✅ XuanFang 3.5" | ✅ [UsbPCMonitor 3.5" / 5"](https://aliexpress.com/item/1005003931363455.html) | ✅ Kipye Qiye Smart Display 3.5" | +|---------------------------------------------------|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------| +| | | | +| revision B & flagship (with backplate & RGB LEDs) | Unknown manufacturer, visually similar to Turing 3.5" / 5". Original software is `UsbPCMonitor.exe` | Front panel has an engraved inscription "奇叶智显" Qiye Zhixian (Qiye Smart Display) | | ✅ WeAct Studio Display FS V1 0.96" | ✅ WeAct Studio Display FS V1 3.5" | |---------------------------------------------------------------|--------------------------------------------------------------| @@ -87,7 +81,7 @@ Some themes are already included for a quick start! * Fully functional multi-OS code base (operates out of the box, tested on Windows, Linux & MacOS). * Display configuration using GUI configuration wizard or `config.yaml` file: no Python code to edit. -* Compatible with [3.5" & 5" smart screen models (Turing, XuanFang...)](https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions). Backplate RGB LEDs are also supported for available models! +* Compatible with [multiple smart screen models (Turing, XuanFang...)](https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions). Backplate RGB LEDs are also supported for available models! * Support [multiple hardware sensors and metrics (CPU/GPU usage, temperatures, memory, disks, etc)](https://github.com/mathoudebine/turing-smart-screen-python/wiki/System-monitor-:-themes#stats-entry) with configurable refresh intervals. * Allow [creation of themes (see `res/themes`) with `theme.yaml` files using theme editor](https://github.com/mathoudebine/turing-smart-screen-python/wiki/System-monitor-:-themes) to be [shared with the community!](https://github.com/mathoudebine/turing-smart-screen-python/discussions/categories/themes) * Easy to expand: [custom Python data sources](https://github.com/mathoudebine/turing-smart-screen-python/wiki/System-monitor-:-themes#add-custom-stats-to-a-theme) can be written to pull specific information and display it on themes like any other sensor. diff --git a/config.yaml b/config.yaml index 84c7bd11..8aef493e 100644 --- a/config.yaml +++ b/config.yaml @@ -2,7 +2,7 @@ config: # Configuration values to set up basic communication # Set your COM port e.g. COM3 for Windows, /dev/ttyACM0 for Linux... - # Use AUTO for COM port auto-discovery (may not work on every setup) + # Use AUTO for COM port auto-discovery (may not work on every setup) or if device is not detected as a COM port # COM_PORT: "/dev/ttyACM0" # COM_PORT: "COM3" COM_PORT: "AUTO" @@ -56,6 +56,7 @@ display: # - B for Xuanfang 3.5" (inc. flagship) # - C for Turing 2.1"/2.8"/5"/8.8" # - D for Kipye Qiye Smart Display 3.5" + # - TUR_USB for Turing HW revisions 1.x: 4.6"/5.2"/8.0"/8.8"/9.2" # - WEACT_A for WeAct Studio Display FS V1 3.5" # - WEACT_B for WeAct Studio Display FS V1 0.96" # - SIMU for simulated display (image written in screencap.png). Width & height will be detected from the theme diff --git a/configure.py b/configure.py index 365a3910..565be5ce 100755 --- a/configure.py +++ b/configure.py @@ -22,9 +22,9 @@ # This file is the system monitor configuration GUI from library.pythoncheck import check_python_version + check_python_version() -import glob import os import platform import subprocess @@ -63,16 +63,36 @@ WEACT_MODEL = "WeAct Studio Display FS V1" SIMULATED_MODEL = "Simulated screen" +_SIZE_2_1_INCH = "2.1\"" # Only for retro compatibility +_SIZE_2_8_INCH = "2.8\"" # Only for retro compatibility +_SIZE_9_2_INCH = "9.2\"" # Only for retro compatibility +SIZE_0_96_INCH = "0.96\"" +SIZE_2_x_INCH = "2.1\" / 2.8\"" SIZE_3_5_INCH = "3.5\"" +SIZE_4_6_INCH = "4.6\"" SIZE_5_INCH = "5\"" +SIZE_5_2_INCH = "5.2\"" +SIZE_8_INCH = "8\"" SIZE_8_8_INCH = "8.8\"" -SIZE_2_1_INCH = "2.1\"" # Only for retro compatibility -SIZE_2_x_INCH = "2.1\" / 2.8\"" -SIZE_0_96_INCH = "0.96\"" - -size_list = (SIZE_0_96_INCH, SIZE_2_x_INCH, SIZE_3_5_INCH, SIZE_5_INCH, SIZE_8_8_INCH) +SIZE_8_8_INCH_NEWREV = "8.8\" / 9.2\" (V1.X new HW rev.)" +SIZE_12_3_INCH = "12.3\"" + +# List of sizes that can be selected +size_list = ( + SIZE_0_96_INCH, + SIZE_2_x_INCH, + SIZE_3_5_INCH, + SIZE_4_6_INCH, + SIZE_5_INCH, + SIZE_5_2_INCH, + SIZE_8_INCH, + SIZE_8_8_INCH, + SIZE_8_8_INCH_NEWREV, + SIZE_12_3_INCH, +) # Maps between config.yaml values and GUI description +# This map is used to select the correct smart screen model based on config.yaml "REVISION" and selected "THEME" size revision_and_size_to_model_map = { ('A', SIZE_3_5_INCH): TURING_MODEL, # Can also be UsbPCMonitor 3.5, does not matter since protocol is the same ('A', SIZE_5_INCH): USBPCMONITOR_MODEL, @@ -81,29 +101,49 @@ ('C', SIZE_5_INCH): TURING_MODEL, ('C', SIZE_8_8_INCH): TURING_MODEL, ('D', SIZE_3_5_INCH): KIPYE_MODEL, + ('TUR_USB', SIZE_4_6_INCH): TURING_MODEL, + ('TUR_USB', SIZE_5_2_INCH): TURING_MODEL, + ('TUR_USB', SIZE_8_INCH): TURING_MODEL, + ('TUR_USB', SIZE_8_8_INCH): TURING_MODEL, + ('TUR_USB', SIZE_8_8_INCH_NEWREV): TURING_MODEL, + ('TUR_USB', SIZE_12_3_INCH): TURING_MODEL, ('WEACT_A', SIZE_3_5_INCH): WEACT_MODEL, ('WEACT_B', SIZE_0_96_INCH): WEACT_MODEL, + ('SIMU', SIZE_0_96_INCH): SIMULATED_MODEL, ('SIMU', SIZE_2_x_INCH): SIMULATED_MODEL, ('SIMU', SIZE_3_5_INCH): SIMULATED_MODEL, + ('SIMU', SIZE_4_6_INCH): SIMULATED_MODEL, ('SIMU', SIZE_5_INCH): SIMULATED_MODEL, + ('SIMU', SIZE_5_2_INCH): SIMULATED_MODEL, + ('SIMU', SIZE_8_INCH): SIMULATED_MODEL, ('SIMU', SIZE_8_8_INCH): SIMULATED_MODEL, } +# This map is used to write the correct config.yaml "REVISION" from selected smart screen model and size model_and_size_to_revision_map = { - (TURING_MODEL, SIZE_3_5_INCH): 'A', - (USBPCMONITOR_MODEL, SIZE_3_5_INCH): 'A', - (USBPCMONITOR_MODEL, SIZE_5_INCH): 'A', - (XUANFANG_MODEL, SIZE_3_5_INCH): 'B', + (KIPYE_MODEL, SIZE_3_5_INCH): 'D', (TURING_MODEL, SIZE_2_x_INCH): 'C', + (TURING_MODEL, SIZE_3_5_INCH): 'A', + (TURING_MODEL, SIZE_4_6_INCH): 'TUR_USB', + (TURING_MODEL, SIZE_5_2_INCH): 'TUR_USB', (TURING_MODEL, SIZE_5_INCH): 'C', + (TURING_MODEL, SIZE_8_INCH): 'TUR_USB', (TURING_MODEL, SIZE_8_8_INCH): 'C', - (KIPYE_MODEL, SIZE_3_5_INCH): 'D', - (WEACT_MODEL, SIZE_3_5_INCH): 'WEACT_A', + (TURING_MODEL, SIZE_8_8_INCH_NEWREV): 'TUR_USB', + (TURING_MODEL, SIZE_12_3_INCH): 'TUR_USB', + (USBPCMONITOR_MODEL, SIZE_3_5_INCH): 'A', + (USBPCMONITOR_MODEL, SIZE_5_INCH): 'A', (WEACT_MODEL, SIZE_0_96_INCH): 'WEACT_B', + (WEACT_MODEL, SIZE_3_5_INCH): 'WEACT_A', + (XUANFANG_MODEL, SIZE_3_5_INCH): 'B', + (SIMULATED_MODEL, SIZE_0_96_INCH): 'SIMU', (SIMULATED_MODEL, SIZE_2_x_INCH): 'SIMU', (SIMULATED_MODEL, SIZE_3_5_INCH): 'SIMU', + (SIMULATED_MODEL, SIZE_4_6_INCH): 'SIMU', (SIMULATED_MODEL, SIZE_5_INCH): 'SIMU', + (SIMULATED_MODEL, SIZE_5_2_INCH): 'SIMU', + (SIMULATED_MODEL, SIZE_8_INCH): 'SIMU', (SIMULATED_MODEL, SIZE_8_8_INCH): 'SIMU', } hw_lib_map = {"AUTO": "Automatic", "LHM": "LibreHardwareMonitor (admin.)", "PYTHON": "Python libraries", @@ -121,20 +161,20 @@ "sk": "Slovak", "sl": "Slovenian", "sp": "Spanish", "sv": "Swedish", "th": "Thai", "tr": "Turkish", "ua": "Ukrainian", "vi": "Vietnamese", "zu": "Zulu"} -MAIN_DIRECTORY = str(Path(__file__).parent.resolve()) + "/" -THEMES_DIR = MAIN_DIRECTORY + 'res/themes' +MAIN_DIRECTORY = Path(__file__).resolve().parent +THEMES_DIR = MAIN_DIRECTORY / "res/themes" + +circular_mask = Image.open(MAIN_DIRECTORY / "res/backgrounds/circular-mask.png") -circular_mask = Image.open(MAIN_DIRECTORY + "res/backgrounds/circular-mask.png") def get_theme_data(name: str): - dir = os.path.join(THEMES_DIR, name) + dir = THEMES_DIR / name + # checking if it is a directory - if os.path.isdir(dir): - # Check if a theme.yaml file exists - theme = os.path.join(dir, 'theme.yaml') - if os.path.isfile(theme): - # Get display size from theme.yaml - with open(theme, "rt", encoding='utf8') as stream: + if dir.is_dir(): + theme = dir / "theme.yaml" + if theme.is_file(): + with open(theme, "rt", encoding="utf8") as stream: theme_data, ind, bsi = ruamel.yaml.util.load_yaml_guess_indent(stream) return theme_data return None @@ -186,8 +226,8 @@ def __init__(self): self.window = Tk() self.window.title('Turing System Monitor configuration') self.window.geometry("820x580") - self.window.iconphoto(True, PhotoImage(file=MAIN_DIRECTORY + "res/icons/monitor-icon-17865/64.png")) - # When window gets focus again, reload theme preview in case it has been updated by theme editor + self.window.iconphoto(True, PhotoImage(file=str( + MAIN_DIRECTORY / "res/icons/monitor-icon-17865/64.png"))) # When window gets focus again, reload theme preview in case it has been updated by theme editor self.window.bind("", self.on_theme_change) self.window.after(0, self.on_fan_speed_update) @@ -287,9 +327,8 @@ def __init__(self): command=lambda: self.on_weatherping_click()) self.weather_ping_btn.place(x=80, y=520, height=50, width=130) - self.open_theme_folder_btn = ttk.Button(self.window, text="Open themes\nfolder", - command=lambda: self.on_open_theme_folder_click()) + command=lambda: self.on_open_theme_folder_click()) self.open_theme_folder_btn.place(x=220, y=520, height=50, width=130) self.edit_theme_btn = ttk.Button(self.window, text="Edit theme", command=lambda: self.on_theme_editor_click()) @@ -311,13 +350,13 @@ def load_theme_preview(self): theme_data = get_theme_data(self.theme_cb.get()) try: - theme_preview = Image.open(MAIN_DIRECTORY + "res/themes/" + self.theme_cb.get() + "/preview.png") + theme_preview = Image.open(MAIN_DIRECTORY / "res" / "themes" / self.theme_cb.get() / "preview.png") - if theme_data['display'].get("DISPLAY_SIZE", '3.5"') == SIZE_2_1_INCH: + if theme_data and theme_data['display'].get("DISPLAY_SIZE", '3.5"') == _SIZE_2_1_INCH: # This is a circular screen: apply a circle mask over the preview theme_preview.paste(circular_mask, mask=circular_mask) except: - theme_preview = Image.open(MAIN_DIRECTORY + "res/docs/no-preview.png") + theme_preview = Image.open(MAIN_DIRECTORY / "res/docs/no-preview.png") finally: theme_preview.thumbnail((320, 480), Image.Resampling.LANCZOS) self.theme_preview_img = ImageTk.PhotoImage(theme_preview) @@ -335,7 +374,7 @@ def load_theme_preview(self): self.theme_author.place(x=10, y=self.theme_preview_img.height() + 15) def load_config_values(self): - with open(MAIN_DIRECTORY + "config.yaml", "rt", encoding='utf8') as stream: + with open(MAIN_DIRECTORY / "config.yaml", "rt", encoding='utf8') as stream: self.config, ind, bsi = ruamel.yaml.util.load_yaml_guess_indent(stream) # Check if theme is valid @@ -381,8 +420,13 @@ def load_config_values(self): # Guess display size from theme in the configuration size = get_theme_size(self.config['config']['THEME']) - size = size.replace(SIZE_2_1_INCH, SIZE_2_x_INCH) # If a theme is for 2.1" then it also is for 2.8" + size = size.replace(_SIZE_2_1_INCH, SIZE_2_x_INCH) # If a theme is for 2.1" then it is for all 2.x" + size = size.replace(_SIZE_2_8_INCH, SIZE_2_x_INCH) # If a theme is for 2.8" then it is for all 2.x" + size = size.replace(_SIZE_9_2_INCH, + SIZE_8_8_INCH_NEWREV) # If a theme is for 9.2" then it is for 8.8"/9.2" (new rev) try: + if size == SIZE_8_8_INCH and self.config['display']['REVISION'] == 'TUR_USB': + size = SIZE_8_8_INCH_NEWREV self.size_cb.set(size) except: self.size_cb.current(0) @@ -445,7 +489,7 @@ def save_config_values(self): self.config['display']['DISPLAY_REVERSE'] = [k for k, v in reverse_map.items() if v == self.orient_cb.get()][0] self.config['display']['BRIGHTNESS'] = int(self.brightness_slider.get()) - with open(MAIN_DIRECTORY + "config.yaml", "w", encoding='utf-8') as file: + with open(MAIN_DIRECTORY / "config.yaml", "w", encoding='utf-8') as file: ruamel.yaml.YAML().dump(self.config, file) def save_additional_config(self, ping: str, api_key: str, lat: str, long: str, unit: str, lang: str): @@ -456,7 +500,7 @@ def save_additional_config(self, ping: str, api_key: str, lat: str, long: str, u self.config['config']['WEATHER_UNITS'] = unit self.config['config']['WEATHER_LANGUAGE'] = lang - with open(MAIN_DIRECTORY + "config.yaml", "w", encoding='utf-8') as file: + with open(MAIN_DIRECTORY / "config.yaml", "w", encoding='utf-8') as file: ruamel.yaml.YAML().dump(self.config, file) def on_theme_change(self, e=None): @@ -466,25 +510,42 @@ def on_weatherping_click(self): self.more_config_window.show() def on_open_theme_folder_click(self): - path = f'"{MAIN_DIRECTORY}res/themes"' + # path = f'"{MAIN_DIRECTORY}res/themes"' + # if platform.system() == "Windows": + # os.startfile(path) + # elif platform.system() == "Darwin": + # subprocess.Popen(["open", path]) + # else: + # subprocess.Popen(["xdg-open", path]) + path = MAIN_DIRECTORY / "res/themes" + if platform.system() == "Windows": os.startfile(path) elif platform.system() == "Darwin": - subprocess.Popen(["open", path]) + subprocess.Popen(["open", str(path)]) else: - subprocess.Popen(["xdg-open", path]) + subprocess.Popen(["xdg-open", str(path)]) def on_theme_editor_click(self): - subprocess.Popen( - f'"{MAIN_DIRECTORY}{glob.glob("theme-editor.*", root_dir=MAIN_DIRECTORY)[0]}" "{self.theme_cb.get()}"', - shell=True) + theme_editor = next(MAIN_DIRECTORY.glob("theme-editor.*")) + + if platform.system() == "Windows": + subprocess.Popen([str(theme_editor), self.theme_cb.get()], shell=True) + else: + subprocess.Popen([str(theme_editor), self.theme_cb.get()]) def on_save_click(self): self.save_config_values() def on_saverun_click(self): self.save_config_values() - subprocess.Popen(f'"{MAIN_DIRECTORY}{glob.glob("main.*", root_dir=MAIN_DIRECTORY)[0]}"', shell=True) + main_file = next(MAIN_DIRECTORY.glob("main.*")) + + if platform.system() == "Windows": + subprocess.Popen([str(main_file)], shell=True) + else: + subprocess.Popen([str(main_file)]) + self.window.destroy() def on_brightness_change(self, e=None): @@ -507,8 +568,18 @@ def on_model_change(self, e=None): def on_size_change(self, e=None): size = self.size_cb.get() - size = size.replace(SIZE_2_x_INCH, SIZE_2_1_INCH) # For '2.1" / 2.8"' size, keep '2.1"' as size to get themes for - themes = get_themes(size) + + # For '2.1" / 2.8"' size, search for themes of both sizes + if size == SIZE_2_x_INCH: + themes = get_themes(_SIZE_2_1_INCH) + themes += get_themes(_SIZE_2_8_INCH) + # For 8.8" & 9.2" sizes, search for themes of both sizes + elif size == SIZE_8_8_INCH_NEWREV or size == SIZE_8_8_INCH: + themes = get_themes(SIZE_8_8_INCH) + themes += get_themes(_SIZE_9_2_INCH) + else: + themes = get_themes(size) + self.theme_cb.config(values=themes) if not self.theme_cb.get() in themes: @@ -627,9 +698,10 @@ def __init__(self, main_window: TuringConfigWindow): self.citysearch1_label = ttk.Label(self.window, text='Location search', font='bold') self.citysearch1_label.place(x=80, y=370) - self.citysearch2_label = ttk.Label(self.window, text="Enter location to automatically get coordinates (latitude/longitude).\n" - "For example \"Berlin\" \"London, GB\", \"London, Quebec\".\n" - "Remember to set valid API key and pick language first!") + self.citysearch2_label = ttk.Label(self.window, + text="Enter location to automatically get coordinates (latitude/longitude).\n" + "For example \"Berlin\" \"London, GB\", \"London, Quebec\".\n" + "Remember to set valid API key and pick language first!") self.citysearch2_label.place(x=10, y=396) self.citysearch3_label = ttk.Label(self.window, text="Enter location") @@ -643,7 +715,8 @@ def __init__(self, main_window: TuringConfigWindow): self.citysearch4_label.place(x=10, y=540) self.citysearch_cb = ttk.Combobox(self.window, values=[], state='readonly') self.citysearch_cb.place(x=140, y=544, width=360) - self.citysearch_btn2 = ttk.Button(self.window, text="Fill in lat/long", command=lambda: self.on_filllatlong_click()) + self.citysearch_btn2 = ttk.Button(self.window, text="Fill in lat/long", + command=lambda: self.on_filllatlong_click()) self.citysearch_btn2.place(x=520, y=540, height=40, width=130) self.citysearch_warn_label = ttk.Label(self.window, text="") @@ -704,10 +777,10 @@ def load_config_values(self, config): self.lang_cb.set(weather_lang_map[self.config['config']['WEATHER_LANGUAGE']]) except: self.lang_cb.set(weather_lang_map["en"]) - + def citysearch_show_warning(self, warning): self.citysearch_warn_label.config(text=warning) - + def on_search_click(self): OPENWEATHER_GEOAPI_URL = "http://api.openweathermap.org/geo/1.0/direct" api_key = self.api_entry.get() @@ -719,8 +792,8 @@ def on_search_click(self): return try: - request = requests.get(OPENWEATHER_GEOAPI_URL, timeout=5, params={"appid": api_key, "lang": lang, - "q": city, "limit": 10}) + request = requests.get(OPENWEATHER_GEOAPI_URL, timeout=5, params={"appid": api_key, "lang": lang, + "q": city, "limit": 10}) except: self.citysearch_show_warning("Error fetching OpenWeatherMap Geo API") return @@ -731,7 +804,7 @@ def on_search_click(self): elif request.status_code != 200: self.citysearch_show_warning(f"Error #{request.status_code} fetching OpenWeatherMap Geo API.") return - + self._city_entries = [] cb_entries = [] for entry in request.json(): @@ -748,7 +821,7 @@ def on_search_click(self): self._city_entries.append({"full_name": full_name, "lat": str(lat), "long": str(long)}) cb_entries.append(full_name) - self.citysearch_cb.config(values = cb_entries) + self.citysearch_cb.config(values=cb_entries) if len(cb_entries) == 0: self.citysearch_show_warning("No given city found.") else: diff --git a/library/display.py b/library/display.py index d4f21009..dabbca52 100644 --- a/library/display.py +++ b/library/display.py @@ -23,6 +23,7 @@ from library.lcd.lcd_comm_rev_a import LcdCommRevA from library.lcd.lcd_comm_rev_b import LcdCommRevB from library.lcd.lcd_comm_rev_c import LcdCommRevC +from library.lcd.lcd_comm_turing_usb import LcdCommTuringUSB from library.lcd.lcd_comm_rev_d import LcdCommRevD from library.lcd.lcd_comm_weact_a import LcdCommWeActA from library.lcd.lcd_comm_weact_b import LcdCommWeActB @@ -57,14 +58,26 @@ def _get_theme_orientation() -> Orientation: def _get_theme_size() -> tuple[int, int]: if config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '0.96"': return 80, 160 - if config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '2.1"': + elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '2.1"': + return 480, 480 + elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '2.8"': return 480, 480 elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '3.5"': return 320, 480 + elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '4.6"': + return 320, 960 elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '5"': return 480, 800 + elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '5.2"': + return 720, 1280 + elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '8"': + return 800, 1280 elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '8.8"': return 480, 1920 + elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '9.2"': + return 480, 1920 # 9.2" displays are 1920x462 but using 1920x480 to be compatible with 8.8" themes + elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '12.3"': + return 720, 1920 else: logger.warning( f'Cannot find valid DISPLAY_SIZE property in selected theme {config.CONFIG_DATA["config"]["THEME"]}, defaulting to 3.5"') @@ -88,6 +101,8 @@ def __init__(self): elif config.CONFIG_DATA["display"]["REVISION"] == "D": self.lcd = LcdCommRevD(com_port=config.CONFIG_DATA['config']['COM_PORT'], update_queue=config.update_queue) + elif config.CONFIG_DATA["display"]["REVISION"] == "TUR_USB": + self.lcd = LcdCommTuringUSB() elif config.CONFIG_DATA["display"]["REVISION"] == "WEACT_A": self.lcd = LcdCommWeActA(com_port=config.CONFIG_DATA['config']['COM_PORT'], update_queue=config.update_queue) diff --git a/library/lcd/lcd_comm.py b/library/lcd/lcd_comm.py index c447486e..f848b8dd 100644 --- a/library/lcd/lcd_comm.py +++ b/library/lcd/lcd_comm.py @@ -50,6 +50,7 @@ def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_hei self.lcd_serial = None # String containing absolute path to serial port e.g. "COM3", "/dev/ttyACM1" or "AUTO" for auto-discovery + # Ignored for USB HID screens self.com_port = com_port # Display always start in portrait orientation by default @@ -180,8 +181,8 @@ def ReadData(self, readSize: int): return self.serial_read(readSize) @staticmethod - @abstractmethod def auto_detect_com_port() -> Optional[str]: + # To implement only for screens that use serial commands pass @abstractmethod diff --git a/library/lcd/lcd_comm_turing_usb.py b/library/lcd/lcd_comm_turing_usb.py new file mode 100644 index 00000000..03875f71 --- /dev/null +++ b/library/lcd/lcd_comm_turing_usb.py @@ -0,0 +1,989 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# +# 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 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 . + +import os +import platform +import queue +import shutil +import struct +import subprocess +import time +from io import BytesIO +from pathlib import Path +from typing import Optional + +import usb.core +import usb.util +from Crypto.Cipher import DES +from PIL import Image + +from library.lcd.lcd_comm import Orientation, LcdComm +from library.log import logger + +VENDOR_ID = 0x1cbe + +# Map of display supported product IDs and their respective resolution in portrait mode +PRODUCT_ID = {0x0046: (320, 960), # Turing 4.6" + 0x0052: (720, 1280), # Turing 5.2" + 0x0080: (800, 1280), # Turing 8.0" + 0x0088: (480, 1920), # Turing 8.8" + 0x0092: (462, 1920), # Turing 9.2" + 0x0123: (720, 1920), # Turing 12.3" +} + +MAX_CHUNK_BYTES = 1024 * 1024 # Data sent to screen cannot exceed 1024MB or there will be a timeout + +# Command IDs used by the vendor protocol (subset) +CMD_UPLOAD_JPEG = 101 +CMD_UPLOAD_PNG = 102 +CMD_GET_H264_CHUNK_SIZE = 17 +CMD_PLAY_H264_CHUNK = 121 +CMD_GET_STREAM_STATUS = 122 +CMD_STOP_STREAM = 123 + +# Default max payload for frame uploads (device/transport limit) +MAX_IMAGE_PAYLOAD_DEFAULT = MAX_CHUNK_BYTES + + +def _resp_ok(resp: Optional[bytes]) -> bool: + if not resp: + return False + b1 = resp[1] if len(resp) > 1 else None + b8 = resp[8] if len(resp) > 8 else None + return (b1 == 0xC8) or (b8 == 0xC8) + + +def send_jpeg(dev, jpeg_data: bytes): + img_size = len(jpeg_data) + cmd_packet = build_command_packet_header(CMD_UPLOAD_JPEG) + cmd_packet[8] = (img_size >> 24) & 0xFF + cmd_packet[9] = (img_size >> 16) & 0xFF + cmd_packet[10] = (img_size >> 8) & 0xFF + cmd_packet[11] = img_size & 0xFF + full_payload = encrypt_command_packet(cmd_packet) + jpeg_data + return write_to_device(dev, full_payload) + + +def _encode_jpeg_under_limit(image: Image.Image, *, max_bytes: int, quality: int = 95, + subsampling: int = -1, ) -> bytes: + if subsampling not in (-1, 0, 1, 2): + raise ValueError("subsampling must be one of: -1, 0, 1, 2") + img = image + if img.mode not in ("RGB", "L"): + img = img.convert("RGB") + elif img.mode == "L": + img = img.convert("RGB") + + subs = (2, 1, 0) if subsampling == -1 else (subsampling,) + best = b"" + for sub in subs: + q = int(quality) + while q >= 1: + buf = BytesIO() + try: + img.save(buf, format="JPEG", quality=q, optimize=False, progressive=False, subsampling=sub, ) + except TypeError: + img.save(buf, format="JPEG", quality=q, optimize=False, progressive=False) + data = buf.getvalue() + if not best or len(data) < len(best): + best = data + if len(data) <= max_bytes: + return data + q = q - 5 if q > 10 else q - 1 + + raise RuntimeError(f"Could not transcode JPEG under max_bytes: {len(best)} > {max_bytes}") + + +def send_pil_image_auto(dev, image: Image.Image, *, max_bytes: int = MAX_IMAGE_PAYLOAD_DEFAULT, ) -> None: + # First try PNG (preferred) + png = _encode_png(image) + if len(png) <= max_bytes: + send_image(dev, png) + return + # Fallback to JPEG when over limit (default behavior) + jpg = _encode_jpeg_under_limit(image, max_bytes=max_bytes, quality=90, subsampling=-1) + send_jpeg(dev, jpg) + + +# ---- MP4 parsing + Annex-B extraction (pure Python fallback) ---- +from dataclasses import dataclass +from typing import Iterable, Set + + +def _u32be(b: bytes, off: int = 0) -> int: + return int.from_bytes(b[off:off + 4], "big", signed=False) + + +def _u64be(b: bytes, off: int = 0) -> int: + return int.from_bytes(b[off:off + 8], "big", signed=False) + + +def _iter_mp4_boxes(data: bytes, start: int, end: int) -> Iterable[tuple[bytes, int, int]]: + i = start + while i + 8 <= end: + size = _u32be(data, i) + typ = data[i + 4:i + 8] + hdr = 8 + if size == 1: + if i + 16 > end: + break + size = _u64be(data, i + 8) + hdr = 16 + elif size == 0: + size = end - i + if size < hdr: + break + j = i + int(size) + if j > end: + break + yield typ, i + hdr, j + i = j + + +def _mp4_find_box(data: bytes, start: int, end: int, typ: bytes) -> Optional[tuple[int, int]]: + for t, ps, pe in _iter_mp4_boxes(data, start, end): + if t == typ: + return ps, pe + return None + + +@dataclass +class _Mp4H264Track: + nal_len_size: int + sps_list: list[bytes] + pps_list: list[bytes] + sample_sizes: list[int] + chunk_offsets: list[int] + stsc: list[tuple[int, int, int]] # (first_chunk, samples_per_chunk, sample_desc_idx) + sync_samples: Optional[Set[int]] + + +def _mp4_parse_avcc(avcc: bytes) -> tuple[int, list[bytes], list[bytes]]: + if len(avcc) < 7: + raise ValueError("avcC too small") + nal_len_size = (avcc[4] & 0x03) + 1 + num_sps = avcc[5] & 0x1F + off = 6 + sps_list: list[bytes] = [] + for _ in range(num_sps): + if off + 2 > len(avcc): + raise ValueError("avcC truncated (SPS length)") + n = int.from_bytes(avcc[off:off + 2], "big") + off += 2 + if off + n > len(avcc): + raise ValueError("avcC truncated (SPS data)") + sps_list.append(avcc[off:off + n]) + off += n + if off + 1 > len(avcc): + raise ValueError("avcC truncated (PPS count)") + num_pps = avcc[off] + off += 1 + pps_list: list[bytes] = [] + for _ in range(num_pps): + if off + 2 > len(avcc): + raise ValueError("avcC truncated (PPS length)") + n = int.from_bytes(avcc[off:off + 2], "big") + off += 2 + if off + n > len(avcc): + raise ValueError("avcC truncated (PPS data)") + pps_list.append(avcc[off:off + n]) + off += n + return nal_len_size, sps_list, pps_list + + +def _mp4_load_moov(path: str) -> bytes: + with open(path, "rb") as f: + f.seek(0, os.SEEK_END) + file_size = f.tell() + f.seek(0, os.SEEK_SET) + while f.tell() + 8 <= file_size: + off0 = f.tell() + hdr = f.read(8) + if len(hdr) < 8: + break + size = _u32be(hdr, 0) + typ = hdr[4:8] + hdr_size = 8 + if size == 1: + ext = f.read(8) + if len(ext) < 8: + break + size = _u64be(ext, 0) + hdr_size = 16 + elif size == 0: + size = file_size - off0 + if size < hdr_size: + break + payload_size = int(size) - hdr_size + if typ == b"moov": + return f.read(payload_size) + f.seek(payload_size, os.SEEK_CUR) + raise ValueError("MP4: moov box not found") + + +def _mp4_pick_h264_video_track(moov: bytes) -> _Mp4H264Track: + moov_start = 0 + moov_end = len(moov) + for t_trak, trak_ps, trak_pe in _iter_mp4_boxes(moov, moov_start, moov_end): + if t_trak != b"trak": + continue + mdia = _mp4_find_box(moov, trak_ps, trak_pe, b"mdia") + if mdia is None: + continue + mdia_ps, mdia_pe = mdia + hdlr = _mp4_find_box(moov, mdia_ps, mdia_pe, b"hdlr") + if hdlr is None: + continue + hdlr_ps, hdlr_pe = hdlr + hdlr_payload = moov[hdlr_ps:hdlr_pe] + if len(hdlr_payload) < 12 or hdlr_payload[8:12] != b"vide": + continue + + minf = _mp4_find_box(moov, mdia_ps, mdia_pe, b"minf") + if minf is None: + continue + stbl = _mp4_find_box(moov, minf[0], minf[1], b"stbl") + if stbl is None: + continue + stbl_ps, stbl_pe = stbl + + stsd = _mp4_find_box(moov, stbl_ps, stbl_pe, b"stsd") + stsz = _mp4_find_box(moov, stbl_ps, stbl_pe, b"stsz") + stsc = _mp4_find_box(moov, stbl_ps, stbl_pe, b"stsc") + stco = _mp4_find_box(moov, stbl_ps, stbl_pe, b"stco") + co64 = _mp4_find_box(moov, stbl_ps, stbl_pe, b"co64") + stss = _mp4_find_box(moov, stbl_ps, stbl_pe, b"stss") + if stsd is None or stsz is None or stsc is None or (stco is None and co64 is None): + continue + + stsd_payload = moov[stsd[0]:stsd[1]] + if len(stsd_payload) < 8: + continue + entry_count = _u32be(stsd_payload, 4) + off = 8 + found = False + nal_len_size = 4 + sps_list: list[bytes] = [] + pps_list: list[bytes] = [] + for _ in range(entry_count): + if off + 8 > len(stsd_payload): + break + ent_size = _u32be(stsd_payload, off) + fmt = stsd_payload[off + 4:off + 8] + ent_end = off + int(ent_size) + if ent_size < 8 or ent_end > len(stsd_payload): + break + if fmt in (b"avc1", b"avc3"): + child_start = off + 8 + 78 + if child_start < ent_end: + for t2, ps2, pe2 in _iter_mp4_boxes(stsd_payload, child_start, ent_end): + if t2 == b"avcC": + nal_len_size, sps_list, pps_list = _mp4_parse_avcc(stsd_payload[ps2:pe2]) + found = True + break + elif fmt in (b"hvc1", b"hev1"): + raise ValueError("MP4 contains HEVC/H.265; device expects H.264") + if found: + break + off = ent_end + if not found: + continue + + stsz_payload = moov[stsz[0]:stsz[1]] + if len(stsz_payload) < 12: + continue + fixed_size = _u32be(stsz_payload, 4) + sample_count = _u32be(stsz_payload, 8) + sample_sizes: list[int] = [] + if fixed_size: + sample_sizes = [int(fixed_size)] * int(sample_count) + else: + need = 12 + int(sample_count) * 4 + if len(stsz_payload) < need: + continue + off2 = 12 + for _ in range(int(sample_count)): + sample_sizes.append(int(_u32be(stsz_payload, off2))) + off2 += 4 + + if stco is not None: + stco_payload = moov[stco[0]:stco[1]] + if len(stco_payload) < 8: + continue + n = _u32be(stco_payload, 4) + need = 8 + int(n) * 4 + if len(stco_payload) < need: + continue + chunk_offsets = [int(_u32be(stco_payload, 8 + 4 * i)) for i in range(int(n))] + else: + co64_payload = moov[co64[0]:co64[1]] # type: ignore[index] + if len(co64_payload) < 8: + continue + n = _u32be(co64_payload, 4) + need = 8 + int(n) * 8 + if len(co64_payload) < need: + continue + chunk_offsets = [int(_u64be(co64_payload, 8 + 8 * i)) for i in range(int(n))] + + stsc_payload = moov[stsc[0]:stsc[1]] + if len(stsc_payload) < 8: + continue + n = _u32be(stsc_payload, 4) + need = 8 + int(n) * 12 + if len(stsc_payload) < need: + continue + stsc_entries: list[tuple[int, int, int]] = [] + off3 = 8 + for _ in range(int(n)): + first_chunk = int(_u32be(stsc_payload, off3)) + samples_per_chunk = int(_u32be(stsc_payload, off3 + 4)) + desc_idx = int(_u32be(stsc_payload, off3 + 8)) + stsc_entries.append((first_chunk, samples_per_chunk, desc_idx)) + off3 += 12 + stsc_entries.sort(key=lambda x: x[0]) + + sync_samples: Optional[Set[int]] = None + if stss is not None: + stss_payload = moov[stss[0]:stss[1]] + if len(stss_payload) >= 8: + n2 = _u32be(stss_payload, 4) + need = 8 + int(n2) * 4 + if len(stss_payload) >= need: + sync_samples = set(int(_u32be(stss_payload, 8 + 4 * i)) for i in range(int(n2))) + + return _Mp4H264Track(nal_len_size=int(nal_len_size), sps_list=sps_list, pps_list=pps_list, + sample_sizes=sample_sizes, chunk_offsets=chunk_offsets, stsc=stsc_entries, sync_samples=sync_samples, ) + + raise ValueError("MP4: no H.264 video track found") + + +def _mp4_iter_sample_locations(track: _Mp4H264Track) -> Iterable[tuple[int, int, int]]: + sizes = track.sample_sizes + sample_idx0 = 0 + entries = track.stsc + entry_idx = 0 + if not sizes: + return + for chunk_idx1, chunk_off in enumerate(track.chunk_offsets, start=1): + while (entry_idx + 1) < len(entries) and chunk_idx1 >= entries[entry_idx + 1][0]: + entry_idx += 1 + samples_per_chunk = entries[entry_idx][1] + off = int(chunk_off) + for _ in range(samples_per_chunk): + if sample_idx0 >= len(sizes): + return + sz = int(sizes[sample_idx0]) + yield sample_idx0 + 1, off, sz + off += sz + sample_idx0 += 1 + + +def _mp4_extract_h264_annexb(in_path: str, out_path: str, *, repeat_headers: bool = True) -> None: + moov = _mp4_load_moov(in_path) + track = _mp4_pick_h264_video_track(moov) + start_code = b"\x00\x00\x00\x01" + spspps = b"".join(start_code + s for s in track.sps_list) + b"".join(start_code + p for p in track.pps_list) + if not spspps: + raise ValueError("MP4: missing SPS/PPS in avcC") + + with open(in_path, "rb") as fin, open(out_path, "wb") as fout: + fout.write(spspps) + nls = int(track.nal_len_size) + if nls not in (1, 2, 3, 4): + raise ValueError(f"MP4: unsupported NAL length size: {nls}") + sync = track.sync_samples + for sample_no, off, sz in _mp4_iter_sample_locations(track): + if repeat_headers and sync is not None and sample_no in sync: + fout.write(spspps) + fin.seek(off, os.SEEK_SET) + data = fin.read(sz) + if len(data) != sz: + raise ValueError("MP4: truncated sample read") + pos = 0 + end = len(data) + while pos + nls <= end: + nal_len = int.from_bytes(data[pos:pos + nls], "big") + pos += nls + if nal_len <= 0: + continue + if pos + nal_len > end: + raise ValueError("MP4: invalid NAL length in sample") + fout.write(start_code) + fout.write(data[pos:pos + nal_len]) + pos += nal_len + + +def build_command_packet_header(a0: int) -> bytearray: + packet = bytearray(500) + packet[0] = a0 + packet[2] = 0x1A + packet[3] = 0x6D + timestamp = int((time.time() - time.mktime(time.localtime()[:3] + (0, 0, 0, 0, 0, -1))) * 1000) + packet[4:8] = struct.pack(' bytes: + cipher = DES.new(key, DES.MODE_CBC, key) + padded_len = (len(data) + 7) // 8 * 8 + padded_data = data.ljust(padded_len, b'\x00') + return cipher.encrypt(padded_data) + + +def encrypt_command_packet(data: bytearray) -> bytearray: + des_key = b'slv3tuzx' + encrypted = encrypt_with_des(des_key, data) + final_packet = bytearray(512) + final_packet[:len(encrypted)] = encrypted + final_packet[510] = 161 + final_packet[511] = 26 + return final_packet + + +def find_usb_device(): + dev = None + dev_pid = None + for pid in PRODUCT_ID.keys(): + dev = usb.core.find(idVendor=VENDOR_ID, idProduct=pid) + dev_pid = pid + if dev is not None: + break + if dev is None: + raise ValueError(f'USB device not found') + + try: + dev.set_configuration() + except usb.core.USBError as e: + print("Warning: set_configuration() failed:", e) + + if platform.system() == "Linux": + try: + if dev.is_kernel_driver_active(0): + dev.detach_kernel_driver(0) + except usb.core.USBError as e: + print("Warning: detach_kernel_driver failed:", e) + + return dev, dev_pid + + +def read_flush(ep_in, max_attempts=5): + """ + Flush the USB IN endpoint by reading available data until timeout or max attempts reached. + """ + for _ in range(max_attempts): + try: + ep_in.read(512, timeout=100) + except usb.core.USBError as e: + if e.errno == 110 or e.args[0] == 'Operation timed out': + break + else: + # print("Flush read error:", e) + break + + +def write_to_device(dev, data, timeout=2000): + cfg = dev.get_active_configuration() + intf = usb.util.find_descriptor(cfg, bInterfaceNumber=0) + if intf is None: + raise RuntimeError("USB interface 0 not found") + ep_out = usb.util.find_descriptor(intf, custom_match=lambda e: usb.util.endpoint_direction( + e.bEndpointAddress) == usb.util.ENDPOINT_OUT) + ep_in = usb.util.find_descriptor(intf, custom_match=lambda e: usb.util.endpoint_direction( + e.bEndpointAddress) == usb.util.ENDPOINT_IN) + assert ep_out is not None and ep_in is not None, "Could not find USB endpoints" + + try: + ep_out.write(data, timeout) + except usb.core.USBError as e: + print("USB write error:", e) + return None + + try: + response = ep_in.read(512, timeout) + read_flush(ep_in) + return bytes(response) + except usb.core.USBError as e: + print("USB read error:", e) + return None + + +def delay_sync(dev): + send_sync_command(dev) + time.sleep(0.2) + + +def send_sync_command(dev): + print("Sending Sync Command (ID 10)...") + cmd_packet = build_command_packet_header(10) + return write_to_device(dev, encrypt_command_packet(cmd_packet)) + + +def send_restart_device_command(dev): + print("Sending Restart Command (ID 11)...") + return write_to_device(dev, encrypt_command_packet(build_command_packet_header(11))) + + +def send_brightness_command(dev, brightness: int): + print(f"Sending Brightness Command (ID 14)...") + print(f" Brightness = {brightness}") + cmd_packet = build_command_packet_header(14) + cmd_packet[8] = brightness + return write_to_device(dev, encrypt_command_packet(cmd_packet)) + + +def send_frame_rate_command(dev, frame_rate: int): + print(f"Sending Frame Rate Command (ID 15)...") + print(f" Frame Rate = {frame_rate}") + cmd_packet = build_command_packet_header(15) + cmd_packet[8] = frame_rate + return write_to_device(dev, encrypt_command_packet(cmd_packet)) + + +def format_bytes(val): + if val > 1024 * 1024: + return f"{val / (1024 * 1024):.2f} GB" + else: + return f"{val / 1024:.2f} MB" + + +def send_refresh_storage_command(dev): + print("Sending Refresh Storage Command (ID 100)...") + response = write_to_device(dev, encrypt_command_packet(build_command_packet_header(100))) + + total = format_bytes(int.from_bytes(response[8:12], byteorder='little')) + used = format_bytes(int.from_bytes(response[12:16], byteorder='little')) + valid = format_bytes(int.from_bytes(response[16:20], byteorder='little')) + + print(f" Card Total = {total}") + print(f" Card Used = {used}") + print(f" Card Valid = {valid}") + + +def send_save_settings_command(dev, brightness=0, startup=0, reserved=0, rotation=0, sleep=0, offline=0): + print("Sending Save Settings Command (ID 125)...") + print(f" Brightness: {brightness}") + print(f" Startup Mode: {startup}") + print(f" Reserved: {reserved}") + print(f" Rotation: {rotation}") + print(f" Sleep Timeout: {sleep}") + print(f" Offline Mode: {offline}") + cmd_packet = build_command_packet_header(125) + cmd_packet[8] = brightness + cmd_packet[9] = startup + cmd_packet[10] = reserved + cmd_packet[11] = rotation + cmd_packet[12] = sleep + cmd_packet[13] = offline + return write_to_device(dev, encrypt_command_packet(cmd_packet)) + + +def send_image(dev, png_data: bytes): + img_size = len(png_data) + + cmd_packet = build_command_packet_header(CMD_UPLOAD_PNG) + cmd_packet[8] = (img_size >> 24) & 0xFF + cmd_packet[9] = (img_size >> 16) & 0xFF + cmd_packet[10] = (img_size >> 8) & 0xFF + cmd_packet[11] = img_size & 0xFF + + full_payload = encrypt_command_packet(cmd_packet) + png_data + return write_to_device(dev, full_payload) + + +def clear_image(dev): + img_data = bytearray( + [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, + 0x01, 0xe0, 0x00, 0x00, 0x07, 0x80, 0x08, 0x06, 0x00, 0x00, 0x00, 0x16, 0xf0, 0x84, 0xf5, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xae, 0xce, 0x1c, 0xe9, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4d, 0x41, + 0x00, 0x00, 0xb1, 0x8f, 0x0b, 0xfc, 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, + 0x0e, 0xc3, 0x00, 0x00, 0x0e, 0xc3, 0x01, 0xc7, 0x6f, 0xa8, 0x64, 0x00, 0x00, 0x0e, 0x0c, 0x49, 0x44, 0x41, + 0x54, 0x78, 0x5e, 0xed, 0xc1, 0x01, 0x0d, 0x00, 0x00, 0x00, 0xc2, 0xa0, 0xf7, 0x4f, 0x6d, 0x0f, 0x07, 0x14, + 0x00, 0x00, 0x00, 0x00, ] + [0x00] * 3568 + [0x00, 0xf0, 0x66, 0x4a, 0xc8, 0x00, 0x01, 0x11, 0x9d, 0x82, 0x0a, + 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, + 0x82]) + img_size = len(img_data) + print(f" Chunk Size: {img_size} bytes") + + cmd_packet = build_command_packet_header(102) + cmd_packet[8] = (img_size >> 24) & 0xFF + cmd_packet[9] = (img_size >> 16) & 0xFF + cmd_packet[10] = (img_size >> 8) & 0xFF + cmd_packet[11] = img_size & 0xFF + + full_payload = encrypt_command_packet(cmd_packet) + img_data + return write_to_device(dev, full_payload) + + +def delay(dev, rst): + time.sleep(0.05) + print("Sending Delay Command (ID 122)...") + cmd_packet = build_command_packet_header(122) + response = write_to_device(dev, encrypt_command_packet(cmd_packet)) + if response and len(response) > 8 and response[8] > rst: + delay(dev, rst) + + +def extract_h264_from_mp4(mp4_path: str): + input_path = Path(mp4_path) + if not input_path.exists(): + raise FileNotFoundError(f"Input file not found: {input_path}") + + output_path = input_path.with_suffix(".h264") + if output_path.exists(): + print(f"{output_path.name} already exists. Skipping extraction.") + return output_path + + # Prefer ffmpeg when available (fast + robust). Fall back to pure-Python MP4->Annex-B extraction. + ffmpeg = shutil.which("ffmpeg") + if ffmpeg: + cmd = [ffmpeg, "-y", "-i", str(input_path), "-c:v", "copy", "-bsf:v", "h264_mp4toannexb", "-an", "-f", "h264", + str(output_path), ] + print(f"Extracting H.264 from {input_path.name} with ffmpeg...") + subprocess.run(cmd, check=True) + print(f"Done. Saved as {output_path.name}") + return output_path + + print(f"ffmpeg not found; extracting H.264 from {input_path.name} with built-in MP4 parser...") + _mp4_extract_h264_annexb(str(input_path), str(output_path), repeat_headers=True) + print(f"Done. Saved as {output_path.name}") + return output_path + + +def send_video(dev, video_path, loop=False): + output_path = extract_h264_from_mp4(video_path) + + write_to_device(dev, encrypt_command_packet(build_command_packet_header(111))) + write_to_device(dev, encrypt_command_packet(build_command_packet_header(112))) + write_to_device(dev, encrypt_command_packet(build_command_packet_header(13))) + send_brightness_command(dev, 32) # 14 + write_to_device(dev, encrypt_command_packet(build_command_packet_header(41))) + clear_image(dev) # 102 + send_frame_rate_command(dev, 25) # 15 + + # Negotiate chunk size if supported + resp = write_to_device(dev, encrypt_command_packet(build_command_packet_header(CMD_GET_H264_CHUNK_SIZE))) + chunk_size = 202752 + try: + if resp and len(resp) >= 12: + negotiated = int.from_bytes(resp[8:12], byteorder="big", signed=False) + if 0 < negotiated <= 1024 * 1024: + chunk_size = negotiated + except Exception: + pass + + print("Sending Send Video Command (ID 121)...") + try: + while True: + with open(output_path, "rb") as f: + while True: + data = f.read(chunk_size) + if not data: + break + + chunksize = len(data) + is_last = f.tell() == os.path.getsize(output_path) + + cmd_packet = build_command_packet_header(CMD_PLAY_H264_CHUNK) + cmd_packet[8] = (chunksize >> 24) & 0xFF + cmd_packet[9] = (chunksize >> 16) & 0xFF + cmd_packet[10] = (chunksize >> 8) & 0xFF + cmd_packet[11] = chunksize & 0xFF + if is_last: + cmd_packet[12] = 1 + + full_payload = encrypt_command_packet(cmd_packet) + data + response = write_to_device(dev, full_payload) + + # Flow control (queue depth is usually reported in response[8] to cmd 122) + if response is None: + delay(dev, 2) + else: + # Poll stream status when queue is high + st = write_to_device(dev, + encrypt_command_packet(build_command_packet_header(CMD_GET_STREAM_STATUS))) + if st and len(st) > 8 and st[8] > 3: + delay(dev, 2) + + print("Video sent successfully.") + if not loop: + break + except KeyboardInterrupt: + print("\nLoop interrupted by user. Sending reset...") + finally: + write_to_device(dev, encrypt_command_packet(build_command_packet_header(CMD_STOP_STREAM))) + + +def _encode_png(image: Image.Image) -> bytes: + buffer = BytesIO() + image.save(buffer, format="PNG", compress_level=9) + return buffer.getvalue() + + +def upload_file(dev, file_path: str) -> bool: + local_path = Path(file_path) + if not local_path.exists(): + logger.error("Error: File does not exist: %s", file_path) + return False + + ext = local_path.suffix.lower() + if ext == ".png": + device_path = f"/tmp/sdcard/mmcblk0p1/img/{local_path.name}" + logger.info("Uploading PNG: %s → %s", file_path, device_path) + elif ext == ".mp4": + h264_path = extract_h264_from_mp4(file_path) + device_path = f"/tmp/sdcard/mmcblk0p1/video/{h264_path.name}" + local_path = h264_path # Update local path to .h264 + logger.info("Uploading MP4 as H264: %s → %s", local_path, device_path) + else: + logger.error("Error: Unsupported file type. Only .png and .mp4 are allowed.") + return False + + if not _open_file_command(dev, device_path): + logger.error("Failed to open remote file for writing.") + return False + + if not _write_file_command(dev, str(local_path)): + logger.error("Failed to write file data.") + return False + + logger.info("Upload completed successfully.") + return True + + +def _open_file_command(dev, path: str): + logger.info("Opening remote file: %s", path) + + path_bytes = path.encode("ascii") + length = len(path_bytes) + + packet = build_command_packet_header(38) + + packet[8] = (length >> 24) & 0xFF + packet[9] = (length >> 16) & 0xFF + packet[10] = (length >> 8) & 0xFF + packet[11] = length & 0xFF + packet[12:16] = b"\x00\x00\x00\x00" + packet[16: 16 + length] = path_bytes + + return write_to_device(dev, encrypt_command_packet(packet)) + + +def _delete_command(dev, file_path: str): + logger.info("Deleting remote file: %s", file_path) + + path_bytes = file_path.encode("ascii") + length = len(path_bytes) + + packet = build_command_packet_header(40) + packet[8] = (length >> 24) & 0xFF + packet[9] = (length >> 16) & 0xFF + packet[10] = (length >> 8) & 0xFF + packet[11] = length & 0xFF + packet[12:16] = b"\x00\x00\x00\x00" + packet[16: 16 + length] = path_bytes + + return write_to_device(dev, encrypt_command_packet(packet)) + + +def _play_command(dev, file_path: str): + logger.info("Requesting playback for: %s", file_path) + + path_bytes = file_path.encode("ascii") + length = len(path_bytes) + + packet = build_command_packet_header(98) + + packet[8] = (length >> 24) & 0xFF + packet[9] = (length >> 16) & 0xFF + packet[10] = (length >> 8) & 0xFF + packet[11] = length & 0xFF + packet[12:16] = b"\x00\x00\x00\x00" + packet[16: 16 + length] = path_bytes + + return write_to_device(dev, encrypt_command_packet(packet)) + + +def _play2_command(dev, file_path: str): + logger.info("Requesting alternate playback for: %s", file_path) + + path_bytes = file_path.encode("ascii") + length = len(path_bytes) + + packet = build_command_packet_header(110) + + packet[8] = (length >> 24) & 0xFF + packet[9] = (length >> 16) & 0xFF + packet[10] = (length >> 8) & 0xFF + packet[11] = length & 0xFF + packet[12:16] = b"\x00\x00\x00\x00" + packet[16: 16 + length] = path_bytes + + return write_to_device(dev, encrypt_command_packet(packet)) + + +def _play3_command(dev, file_path: str): + logger.info("Requesting image playback for: %s", file_path) + + path_bytes = file_path.encode("ascii") + length = len(path_bytes) + + packet = build_command_packet_header(113) + + packet[8] = (length >> 24) & 0xFF + packet[9] = (length >> 16) & 0xFF + packet[10] = (length >> 8) & 0xFF + packet[11] = length & 0xFF + packet[12:16] = b"\x00\x00\x00\x00" + packet[16: 16 + length] = path_bytes + + return write_to_device(dev, encrypt_command_packet(packet)) + + +def _write_file_command(dev, file_path: str) -> bool: + logger.info("Writing remote file from: %s", file_path) + + try: + total_size = Path(file_path).stat().st_size + sent = 0 + chunk_index = 0 + + preferred_cap = min(1024 * 1024, MAX_CHUNK_BYTES) + + with open(file_path, "rb") as fh: + while True: + data_chunk = fh.read(preferred_cap) + if not data_chunk: + break + + chunk_index += 1 + chunk_len = len(data_chunk) + sent += chunk_len + is_last = sent >= total_size + + # [8..11]=chunk_capacity, [12..15]=chunk_len, [16]=last_flag, payload=chunk + cmd_packet = build_command_packet_header(39) + cap = preferred_cap + cmd_packet[8] = (cap >> 24) & 0xFF + cmd_packet[9] = (cap >> 16) & 0xFF + cmd_packet[10] = (cap >> 8) & 0xFF + cmd_packet[11] = cap & 0xFF + cmd_packet[12] = (chunk_len >> 24) & 0xFF + cmd_packet[13] = (chunk_len >> 16) & 0xFF + cmd_packet[14] = (chunk_len >> 8) & 0xFF + cmd_packet[15] = chunk_len & 0xFF + if is_last: + cmd_packet[16] = 1 + + response = write_to_device(dev, encrypt_command_packet(cmd_packet) + data_chunk) + + # Fallback: legacy layout uses [8..11]=chunk_len only + if response is None or (not _resp_ok(response)): + legacy_packet = build_command_packet_header(39) + legacy_packet[8] = (chunk_len >> 24) & 0xFF + legacy_packet[9] = (chunk_len >> 16) & 0xFF + legacy_packet[10] = (chunk_len >> 8) & 0xFF + legacy_packet[11] = chunk_len & 0xFF + response = write_to_device(dev, encrypt_command_packet(legacy_packet) + data_chunk) + + if response is None: + logger.error("Write command failed at chunk %d", chunk_index) + return False + + logger.info("File write completed successfully (%d chunks).", chunk_index) + return True + except FileNotFoundError: + logger.error("File not found: %s", file_path) + return False + except Exception as exc: + logger.error("Error writing file: %s", exc) + return False + + +# This class is for Turing Smart Screen newer models (4.6" / 5.2" / 8" / 8.8" HW rev 1.x / 9.2" / 12.3") +# These models are not detected as serial ports but as (Win)USB devices +class LcdCommTuringUSB(LcdComm): + def __init__(self, com_port: str = "AUTO", display_width: int = 480, display_height: int = 1920, + update_queue: Optional[queue.Queue] = None): + super().__init__(com_port, display_width, display_height, update_queue) + self.dev, self.dev_pid = find_usb_device() + self.display_width, self.display_height = PRODUCT_ID[self.dev_pid] + # Store the current screen state as an image that will be continuously updated and sent + self.current_state = Image.new("RGBA", (self.get_width(), self.get_height()), (0, 0, 0, 0)) + + def InitializeComm(self): + send_sync_command(self.dev) + + def Reset(self): + # Do not enable the reset command for now on Turing USB models + # send_restart_device_command(self.dev) + pass + + def Clear(self): + clear_image(self.dev) + + def ScreenOff(self): + # Turing USB models do not implement a "screen off" command (that we know of): use SetBrightness(0) instead + self.Clear() + self.SetBrightness(0) + + def ScreenOn(self): + # Turing USB models do not implement a "screen off" command (that we know of): using SetBrightness() instead + self.SetBrightness() + + def SetBrightness(self, level: int = 25): + assert 0 <= level <= 100, 'Brightness level must be [0-100]' + converted = int(level / 100 * 102) + send_brightness_command(self.dev, converted) + + def SetOrientation(self, orientation: Orientation): + self.orientation = orientation + # Recreate new state with correct width/height now that screen orientation has changed + self.current_state = Image.new("RGBA", (self.get_width(), self.get_height()), (0, 0, 0, 0)) + + def DisplayPILImage(self, image: Image.Image, x: int = 0, y: int = 0, image_width: int = 0, image_height: int = 0): + # If the image height/width isn't provided, use the native image size + if not image_height: + image_height = image.size[1] + if not image_width: + image_width = image.size[0] + + if image.size[1] > self.get_height(): + image_height = self.get_height() + if image.size[0] > self.get_width(): + image_width = self.get_width() + + if image_width != image.size[0] or image_height != image.size[1]: + image = image.crop((0, 0, image_width, image_height)) + + # Paste new image over existing screen state + self.current_state.paste(image, (x, y)) + + # Rotate image before sending to screen: all images sent to the screen are in portrait mode + if self.orientation == Orientation.LANDSCAPE: + base_image = self.current_state.transpose(Image.Transpose.ROTATE_270) + elif self.orientation == Orientation.REVERSE_LANDSCAPE: + base_image = self.current_state.transpose(Image.Transpose.ROTATE_90) + elif self.orientation == Orientation.PORTRAIT: + base_image = self.current_state.transpose(Image.Transpose.ROTATE_180) + else: # Orientation.REVERSE_PORTRAIT is initial screen orientation + base_image = self.current_state + + # Send image data (auto JPEG fallback when payload exceeds device limit) + send_pil_image_auto(self.dev, base_image, max_bytes=MAX_IMAGE_PAYLOAD_DEFAULT) diff --git a/main.py b/main.py index 38676496..9872f533 100755 --- a/main.py +++ b/main.py @@ -68,7 +68,7 @@ # If pystray cannot be loaded do not stop the program, just ignore it. The tray icon will not be displayed. pass -MAIN_DIRECTORY = str(Path(__file__).parent.resolve()) + "/" +MAIN_DIRECTORY = Path(__file__).resolve().parent if __name__ == "__main__": @@ -110,17 +110,20 @@ def clean_stop(tray_icon=None): except: os._exit(0) - def on_signal_caught(signum, frame=None): logger.info("Caught signal %d, exiting" % signum) clean_stop() - def on_configure_tray(tray_icon, item): logger.info("Configure from tray icon") - subprocess.Popen(f'"{MAIN_DIRECTORY}{glob.glob("configure.*", root_dir=MAIN_DIRECTORY)[0]}"', shell=True) - clean_stop(tray_icon) + configure_file = next(MAIN_DIRECTORY.glob("configure.*")) + + if platform.system() == "Windows": + subprocess.Popen([str(configure_file)], shell=True) + else: + subprocess.Popen([str(configure_file)]) + clean_stop(tray_icon) def on_exit_tray(tray_icon, item): logger.info("Exit from tray icon") @@ -165,7 +168,7 @@ def on_win32_wm_event(hWnd, msg, wParam, lParam): tray_icon = pystray.Icon( name='Turing System Monitor', title='Turing System Monitor', - icon=Image.open(MAIN_DIRECTORY + "res/icons/monitor-icon-17865/64.png"), + icon=Image.open(MAIN_DIRECTORY / "res/icons/monitor-icon-17865/64.png"), menu=pystray.Menu( pystray.MenuItem( text='Configure', diff --git a/requirements.txt b/requirements.txt index 7f65f475..e7c19de9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,8 @@ tkinter-tooltip~=3.1.2 # Tooltips for configuration editor uptime~=3.0.1 # For System Uptime ping3~=5.1.5 # ICMP ping implementation using raw socket pyinstaller~=6.19.0 # bundles a Python application and all its dependencies into a single package +pyusb~=1.3.1 +pycryptodome~=3.23.0 # HTTP library requests~=2.32.5; python_version < "3.10" diff --git a/res/backgrounds/example_1920x480.png b/res/backgrounds/example_1920x480.png new file mode 100644 index 00000000..65e3cad4 Binary files /dev/null and b/res/backgrounds/example_1920x480.png differ diff --git a/res/docs/turing46inch.png b/res/docs/turing46inch.png new file mode 100644 index 00000000..7bfe6b83 Binary files /dev/null and b/res/docs/turing46inch.png differ diff --git a/res/docs/turing5inch.png b/res/docs/turing5inch.png index 7de8ff6f..fff33164 100644 Binary files a/res/docs/turing5inch.png and b/res/docs/turing5inch.png differ diff --git a/res/docs/turing8inch.png b/res/docs/turing8inch.png new file mode 100644 index 00000000..a518329b Binary files /dev/null and b/res/docs/turing8inch.png differ diff --git a/res/themes/8inchTheme2/background.png b/res/themes/8inchTheme2/background.png new file mode 100644 index 00000000..84a62fd1 Binary files /dev/null and b/res/themes/8inchTheme2/background.png differ diff --git a/res/themes/8inchTheme2/preview.png b/res/themes/8inchTheme2/preview.png new file mode 100644 index 00000000..85147271 Binary files /dev/null and b/res/themes/8inchTheme2/preview.png differ diff --git a/res/themes/8inchTheme2/theme.yaml b/res/themes/8inchTheme2/theme.yaml new file mode 100644 index 00000000..6c94b11d --- /dev/null +++ b/res/themes/8inchTheme2/theme.yaml @@ -0,0 +1,255 @@ +--- +author: "@alexwbaule" + +display: + DISPLAY_SIZE: 8" + DISPLAY_ORIENTATION: portrait + DISPLAY_RGB_LED: 0, 0, 255 + +static_images: + BACKGROUND: + PATH: background.png + X: 0 + Y: 0 + WIDTH: 800 + HEIGHT: 1280 +static_text: + DISK_USED_LABEL: + TEXT: "Used:" + X: 207 + Y: 984 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + DISK_FREE_LABEL: + TEXT: "Free:" + X: 207 + Y: 1070 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + DISK_TOTAL_LABEL: + TEXT: "Total:" + X: 207 + Y: 1027 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 +STATS: + CPU: + PERCENTAGE: + INTERVAL: 1 + TEXT: + SHOW: True + SHOW_UNIT: True + X: 475 + Y: 64 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + GRAPH: + SHOW: True + X: 500 + Y: 112 + WIDTH: 250 + HEIGHT: 24 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 0, 0, 255 + BAR_OUTLINE: False + BACKGROUND_COLOR: 0, 0, 0 + FREQUENCY: + INTERVAL: 5 + TEXT: + SHOW: True + SHOW_UNIT: True + X: 500 + Y: 160 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 49 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + TEMPERATURE: + INTERVAL: 5 + TEXT: + SHOW: True + SHOW_UNIT: True + X: 333 + Y: 112 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + GPU: + INTERVAL: 1 + PERCENTAGE: + GRAPH: + SHOW: True + X: 500 + Y: 416 + WIDTH: 250 + HEIGHT: 24 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 0, 0, 255 + BAR_OUTLINE: False + BACKGROUND_COLOR: 0, 0, 0 + TEXT: + SHOW: TRUE + SHOW_UNIT: True + X: 475 + Y: 368 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + MEMORY: + TEXT: + SHOW: TRUE + SHOW_UNIT: True + X: 500 + Y: 464 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + TEMPERATURE: + TEXT: + SHOW: TRUE + SHOW_UNIT: True + X: 333 + Y: 416 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + MEMORY: + INTERVAL: 5 + VIRTUAL: + GRAPH: + SHOW: True + X: 500 + Y: 720 + WIDTH: 250 + HEIGHT: 24 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 0, 0, 255 + BAR_OUTLINE: True + BACKGROUND_COLOR: 0, 0, 0 + USED: + SHOW: TRUE + SHOW_UNIT: True + X: 500 + Y: 768 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + PERCENT_TEXT: + SHOW: TRUE + SHOW_UNIT: True + X: 475 + Y: 672 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + DISK: + INTERVAL: 10 + USED: + GRAPH: + SHOW: True + X: 207 + Y: 944 + WIDTH: 297 + HEIGHT: 24 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 0, 0, 255 + BAR_OUTLINE: False + BACKGROUND_COLOR: 0, 0, 0 + TEXT: + SHOW: TRUE + SHOW_UNIT: True + X: 340 + Y: 984 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + TOTAL: + TEXT: + SHOW: TRUE + SHOW_UNIT: True + X: 340 + Y: 1027 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + FREE: + TEXT: + SHOW: TRUE + SHOW_UNIT: True + X: 340 + Y: 1070 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + + DATE: + INTERVAL: 1 + DAY: + TEXT: + SHOW: True + X: 25 + Y: 1184 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 41 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + WIDTH: 383 + ANCHOR: lt + HOUR: + TEXT: + SHOW: True + X: 25 + Y: 1229 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 41 + FONT_COLOR: 255, 255, 255 + BACKGROUND_COLOR: 132, 154, 165 + WIDTH: 383 + ANCHOR: lt + + WEATHER: + # For optimal use, if you don't want to trigger the free threshold daily call (1000 calls), the interval should be 90 MINIMUM (not really useful as the API didn't update that quickly) + INTERVAL: 300 + TEMPERATURE: + TEXT: + SHOW: True + X: 450 + Y: 1229 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 41 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + WIDTH: 333 + ANCHOR: rt + WEATHER_DESCRIPTION: + TEXT: + SHOW: True + X: 450 + Y: 1184 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 41 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + WIDTH: 333 + ANCHOR: rt diff --git a/res/themes/8inchTheme2/theme_example.png b/res/themes/8inchTheme2/theme_example.png new file mode 100644 index 00000000..cafeed38 Binary files /dev/null and b/res/themes/8inchTheme2/theme_example.png differ diff --git a/res/themes/ColoredFlat_5.2inch/background.png b/res/themes/ColoredFlat_5.2inch/background.png new file mode 100644 index 00000000..758f938e Binary files /dev/null and b/res/themes/ColoredFlat_5.2inch/background.png differ diff --git a/res/themes/ColoredFlat_5.2inch/background.xcf b/res/themes/ColoredFlat_5.2inch/background.xcf new file mode 100644 index 00000000..e8b0ba5f Binary files /dev/null and b/res/themes/ColoredFlat_5.2inch/background.xcf differ diff --git a/res/themes/ColoredFlat_5.2inch/preview.png b/res/themes/ColoredFlat_5.2inch/preview.png new file mode 100644 index 00000000..2b84f0b8 Binary files /dev/null and b/res/themes/ColoredFlat_5.2inch/preview.png differ diff --git a/res/themes/ColoredFlat_5.2inch/telecharger-arrow.png b/res/themes/ColoredFlat_5.2inch/telecharger-arrow.png new file mode 100644 index 00000000..691717a4 Binary files /dev/null and b/res/themes/ColoredFlat_5.2inch/telecharger-arrow.png differ diff --git a/res/themes/ColoredFlat_5.2inch/theme.yaml b/res/themes/ColoredFlat_5.2inch/theme.yaml new file mode 100644 index 00000000..621f96f1 --- /dev/null +++ b/res/themes/ColoredFlat_5.2inch/theme.yaml @@ -0,0 +1,583 @@ +--- +author: "@Psykotik" + +display: + DISPLAY_SIZE: 5.2" + DISPLAY_ORIENTATION: portrait + DISPLAY_RGB_LED: 255, 0, 0 + +static_images: + BACKGROUND: + PATH: background.png + X: 0 + Y: 0 + WIDTH: 720 + HEIGHT: 1280 + +static_text: + RAM: + TEXT: /32Gb + X: 180 + Y: 557 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + +STATS: + CPU: + PERCENTAGE: + INTERVAL: 1 + TEXT: + SHOW: true + SHOW_UNIT: true + X: 165 + Y: 203 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 55 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + GRAPH: + SHOW: false + X: 60 + Y: 336 + WIDTH: 240 + HEIGHT: 24 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 247, 227, 227 + BAR_OUTLINE: true + BACKGROUND_IMAGE: background.png + LINE_GRAPH: + SHOW: true + X: 45 + Y: 256 + WIDTH: 270 + HEIGHT: 112 + MIN_VALUE: 0 + MAX_VALUE: 100 + HISTORY_SIZE: 60 + AUTOSCALE: false + LINE_COLOR: 247, 227, 227 + AXIS: true + AXIS_COLOR: 247, 227, 227 + AXIS_FONT: roboto/Roboto-Black.ttf + AXIS_FONT_SIZE: 16 + BACKGROUND_IMAGE: background.png + FREQUENCY: + INTERVAL: 1 + TEXT: + SHOW: true + SHOW_UNIT: True + X: 60 + Y: 400 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 24 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + LOAD: + INTERVAL: 1 + ONE: + TEXT: + SHOW: false + SHOW_UNIT: false + X: 165 + Y: 170 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 21 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.png + FIVE: + TEXT: + SHOW: false + SHOW_UNIT: false + X: 274 + Y: 170 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 21 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.png + FIFTEEN: + TEXT: + SHOW: False + SHOW_UNIT: false + X: 398 + Y: 170 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 21 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.png + TEMPERATURE: + INTERVAL: 1 + TEXT: + SHOW: true + SHOW_UNIT: True + X: 165 + Y: 384 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 48 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + GPU: + INTERVAL: 1 + PERCENTAGE: + TEXT: + SHOW: true + SHOW_UNIT: True + X: 518 + Y: 203 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 55 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + GRAPH: + SHOW: false + X: 420 + Y: 336 + WIDTH: 240 + HEIGHT: 24 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 247, 227, 227 + BAR_OUTLINE: true + BACKGROUND_IMAGE: background.png + LINE_GRAPH: + SHOW: true + X: 405 + Y: 256 + WIDTH: 270 + HEIGHT: 112 + MIN_VALUE: 0 + MAX_VALUE: 100 + HISTORY_SIZE: 60 + AUTOSCALE: false + LINE_COLOR: 247, 227, 227 + AXIS: True + AXIS_COLOR: 247, 227, 227 + AXIS_FONT: roboto/Roboto-Black.ttf + AXIS_FONT_SIZE: 16 + BACKGROUND_IMAGE: background.png + MEMORY: + GRAPH: + SHOW: true + X: 420 + Y: 416 + WIDTH: 105 + HEIGHT: 16 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 247, 227, 227 + BAR_OUTLINE: true + BACKGROUND_IMAGE: background.png + RADIAL: + SHOW: false + X: 465 + Y: 408 + RADIUS: 30 + WIDTH: 7 + MIN_VALUE: 0 + MAX_VALUE: 100 + ANGLE_START: 110 + ANGLE_END: 70 + ANGLE_STEPS: 1 + ANGLE_SEP: 25 + CLOCKWISE: True + BAR_COLOR: 247, 227, 227 + SHOW_TEXT: True + SHOW_UNIT: True + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 24 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.png + TEXT: + SHOW: true + SHOW_UNIT: True + X: 420 + Y: 392 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 24 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + TEMPERATURE: + TEXT: + SHOW: true + SHOW_UNIT: True + X: 525 + Y: 384 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 48 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + MEMORY: + INTERVAL: 1 + SWAP: + GRAPH: + SHOW: false + X: 173 + Y: 456 + WIDTH: 267 + HEIGHT: 21 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 255, 0, 0 + BAR_OUTLINE: False + BACKGROUND_IMAGE: background.png + RADIAL: + SHOW: False + X: 212 + Y: 440 + RADIUS: 42 + WIDTH: 12 + MIN_VALUE: 0 + MAX_VALUE: 100 + ANGLE_START: 110 + ANGLE_END: 70 + ANGLE_STEPS: 1 + ANGLE_SEP: 25 + CLOCKWISE: True + BAR_COLOR: 255, 0, 0 + SHOW_TEXT: True + SHOW_UNIT: True + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 21 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.png + VIRTUAL: + GRAPH: + SHOW: false + X: 60 + Y: 672 + WIDTH: 240 + HEIGHT: 24 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 247, 227, 227 + BAR_OUTLINE: true + BACKGROUND_IMAGE: background.png + LINE_GRAPH: + SHOW: True + X: 45 + Y: 608 + WIDTH: 270 + HEIGHT: 112 + MIN_VALUE: 0 + MAX_VALUE: 100 + HISTORY_SIZE: 200 + AUTOSCALE: False + LINE_COLOR: 247, 227, 227 + AXIS: true + AXIS_COLOR: 247, 227, 227 + AXIS_FONT: roboto/Roboto-Black.ttf + AXIS_FONT_SIZE: 16 + BACKGROUND_IMAGE: background.png + USED: + SHOW: True + SHOW_UNIT: false + X: 83 + Y: 560 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + FREE: + SHOW: FALSE + SHOW_UNIT: True + X: 273 + Y: 206 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 36 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + PERCENT_TEXT: + SHOW: True + SHOW_UNIT: True + X: 165 + Y: 496 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 55 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + DISK: + INTERVAL: 10 + USED: + GRAPH: + SHOW: true + X: 420 + Y: 640 + WIDTH: 240 + HEIGHT: 24 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 247, 227, 227 + BAR_OUTLINE: true + BACKGROUND_IMAGE: background.png + RADIAL: + SHOW: false + X: 540 + Y: 664 + RADIUS: 52 + WIDTH: 9 + MIN_VALUE: 0 + MAX_VALUE: 100 + ANGLE_START: 120 + ANGLE_END: 70 + ANGLE_STEPS: 1 + ANGLE_SEP: 25 + CLOCKWISE: True + BAR_COLOR: 247, 227, 227 + SHOW_TEXT: True + SHOW_UNIT: True + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 40 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + TEXT: + SHOW: false + SHOW_UNIT: True + X: 306 + Y: 648 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 36 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + PERCENT_TEXT: + SHOW: true + SHOW_UNIT: True + X: 457 + Y: 576 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 64 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + TOTAL: + TEXT: + SHOW: False + SHOW_UNIT: True + X: 306 + Y: 600 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 36 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + FREE: + TEXT: + SHOW: False + SHOW_UNIT: True + X: 306 + Y: 696 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 36 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + NET: + INTERVAL: 1 + WLO: + UPLOAD: + TEXT: + SHOW: false + X: 490 + Y: 944 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + UPLOADED: + TEXT: + SHOW: false + X: 570 + Y: 992 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 27 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + DOWNLOAD: + TEXT: + SHOW: false + X: 130 + Y: 944 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + DOWNLOADED: + TEXT: + SHOW: false + X: 210 + Y: 992 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 27 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + ETH: + UPLOAD: + TEXT: + SHOW: true + X: 472 + Y: 864 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + LINE_GRAPH: + SHOW: true + X: 390 + Y: 960 + WIDTH: 270 + HEIGHT: 96 + MIN_VALUE: 0 + MAX_VALUE: 500000 + HISTORY_SIZE: 120 + AUTOSCALE: False + LINE_COLOR: 247, 227, 227 + AXIS: True + AXIS_COLOR: 247, 227, 227 + AXIS_FONT: roboto/Roboto-Black.ttf + AXIS_FONT_SIZE: 16 + BACKGROUND_IMAGE: background.png + UPLOADED: + TEXT: + SHOW: true + X: 555 + Y: 912 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 27 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + DOWNLOAD: + TEXT: + SHOW: true + X: 120 + Y: 864 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + LINE_GRAPH: + SHOW: true + X: 45 + Y: 960 + WIDTH: 270 + HEIGHT: 96 + MIN_VALUE: 0 + MAX_VALUE: 1500000 + HISTORY_SIZE: 120 + AUTOSCALE: False + LINE_COLOR: 247, 227, 227 + AXIS: True + AXIS_COLOR: 247, 227, 227 + AXIS_FONT: roboto/Roboto-Black.ttf + AXIS_FONT_SIZE: 16 + BACKGROUND_IMAGE: background.png + DOWNLOADED: + TEXT: + SHOW: true + X: 202 + Y: 912 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 27 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + DATE: + # For time display, it is recommended not to change the interval: keep to 1 + INTERVAL: 1 + DAY: # Format (Y/M/D ordering, month/day translations...) will match your computer locale + TEXT: + FORMAT: medium # short (2/20/23) / medium (Feb 20, 2023) / long (February 20, 2023) / full (Monday, February 20, 2023) + SHOW: True + X: 360 + Y: 40 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 48 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + WIDTH: 360 + ANCHOR: mt + HOUR: # Format (12/24h, timezone translations) will match your computer locale + TEXT: + FORMAT: medium # short (6:48 PM) / medium (6:48:53 PM) / long (6:48:53 PM UTC) / full (6:48:53 PM Coordinated Universal Time) + SHOW: True + X: 0 + Y: 40 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 48 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + WIDTH: 360 + ANCHOR: mt + WEATHER: + # For optimal use, if you don't want to trigger the free threshold daily call (1000 calls), the interval should be 90 MINIMUM (not really useful as the API didn't update that quickly) + INTERVAL: 300 + TEMPERATURE: + TEXT: + SHOW: True + X: 40 + Y: 112 + FONT: roboto-mono/RobotoMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + TEMPERATURE_FELT: + TEXT: + SHOW: True + X: 173 + Y: 109 + FONT: roboto-mono/RobotoMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + UPDATE_TIME: + TEXT: + SHOW: True + X: 555 + Y: 110 + FONT: roboto-mono/RobotoMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + HUMIDITY: + TEXT: + SHOW: True + X: 435 + Y: 110 + FONT: roboto-mono/RobotoMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + WEATHER_DESCRIPTION: + TEXT: + SHOW: True + X: 0 + Y: 152 + WIDTH: 720 + FONT: roboto-mono/RobotoMono-Bold.ttf + FONT_SIZE: 28 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + ANCHOR: mb # Text is centered + PING: + INTERVAL: 1 + TEXT: + SHOW: True + X: 488 + Y: 792 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 40 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + ALIGN: right + LINE_GRAPH: + SHOW: true + X: 45 + Y: 1088 + WIDTH: 623 + HEIGHT: 112 + MIN_VALUE: 0 + MAX_VALUE: 250 + HISTORY_SIZE: 120 + AUTOSCALE: false + LINE_COLOR: 247, 227, 227 + AXIS: True + AXIS_COLOR: 247, 227, 227 + AXIS_FONT: roboto/Roboto-Black.ttf + AXIS_FONT_SIZE: 16 + BACKGROUND_IMAGE: background.png diff --git a/res/themes/ColoredFlat_5.2inch/wallpaperv2.png b/res/themes/ColoredFlat_5.2inch/wallpaperv2.png new file mode 100644 index 00000000..bf2d7581 Binary files /dev/null and b/res/themes/ColoredFlat_5.2inch/wallpaperv2.png differ diff --git a/res/themes/ColoredFlat_8inch/background.png b/res/themes/ColoredFlat_8inch/background.png new file mode 100644 index 00000000..a2c34b4d Binary files /dev/null and b/res/themes/ColoredFlat_8inch/background.png differ diff --git a/res/themes/ColoredFlat_8inch/background.xcf b/res/themes/ColoredFlat_8inch/background.xcf new file mode 100644 index 00000000..e8b0ba5f Binary files /dev/null and b/res/themes/ColoredFlat_8inch/background.xcf differ diff --git a/res/themes/ColoredFlat_8inch/preview.png b/res/themes/ColoredFlat_8inch/preview.png new file mode 100644 index 00000000..8f61f86e Binary files /dev/null and b/res/themes/ColoredFlat_8inch/preview.png differ diff --git a/res/themes/ColoredFlat_8inch/telecharger-arrow.png b/res/themes/ColoredFlat_8inch/telecharger-arrow.png new file mode 100644 index 00000000..691717a4 Binary files /dev/null and b/res/themes/ColoredFlat_8inch/telecharger-arrow.png differ diff --git a/res/themes/ColoredFlat_8inch/theme.yaml b/res/themes/ColoredFlat_8inch/theme.yaml new file mode 100644 index 00000000..b55ba6c3 --- /dev/null +++ b/res/themes/ColoredFlat_8inch/theme.yaml @@ -0,0 +1,583 @@ +--- +author: "@Psykotik" + +display: + DISPLAY_SIZE: 8" + DISPLAY_ORIENTATION: portrait + DISPLAY_RGB_LED: 255, 0, 0 + +static_images: + BACKGROUND: + PATH: background.png + X: 0 + Y: 0 + WIDTH: 800 + HEIGHT: 1280 + +static_text: + RAM: + TEXT: /32Gb + X: 200 + Y: 557 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 37 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + +STATS: + CPU: + PERCENTAGE: + INTERVAL: 1 + TEXT: + SHOW: true + SHOW_UNIT: true + X: 183 + Y: 203 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 58 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + GRAPH: + SHOW: false + X: 67 + Y: 336 + WIDTH: 267 + HEIGHT: 24 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 247, 227, 227 + BAR_OUTLINE: true + BACKGROUND_IMAGE: background.png + LINE_GRAPH: + SHOW: true + X: 50 + Y: 256 + WIDTH: 300 + HEIGHT: 112 + MIN_VALUE: 0 + MAX_VALUE: 100 + HISTORY_SIZE: 60 + AUTOSCALE: false + LINE_COLOR: 247, 227, 227 + AXIS: true + AXIS_COLOR: 247, 227, 227 + AXIS_FONT: roboto/Roboto-Black.ttf + AXIS_FONT_SIZE: 17 + BACKGROUND_IMAGE: background.png + FREQUENCY: + INTERVAL: 1 + TEXT: + SHOW: true + SHOW_UNIT: True + X: 67 + Y: 400 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 25 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + LOAD: + INTERVAL: 1 + ONE: + TEXT: + SHOW: false + SHOW_UNIT: false + X: 183 + Y: 170 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 22 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.png + FIVE: + TEXT: + SHOW: false + SHOW_UNIT: false + X: 305 + Y: 170 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 22 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.png + FIFTEEN: + TEXT: + SHOW: False + SHOW_UNIT: false + X: 442 + Y: 170 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 22 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.png + TEMPERATURE: + INTERVAL: 1 + TEXT: + SHOW: true + SHOW_UNIT: True + X: 183 + Y: 384 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 50 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + GPU: + INTERVAL: 1 + PERCENTAGE: + TEXT: + SHOW: true + SHOW_UNIT: True + X: 575 + Y: 203 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 58 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + GRAPH: + SHOW: false + X: 467 + Y: 336 + WIDTH: 267 + HEIGHT: 24 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 247, 227, 227 + BAR_OUTLINE: true + BACKGROUND_IMAGE: background.png + LINE_GRAPH: + SHOW: true + X: 450 + Y: 256 + WIDTH: 300 + HEIGHT: 112 + MIN_VALUE: 0 + MAX_VALUE: 100 + HISTORY_SIZE: 60 + AUTOSCALE: false + LINE_COLOR: 247, 227, 227 + AXIS: True + AXIS_COLOR: 247, 227, 227 + AXIS_FONT: roboto/Roboto-Black.ttf + AXIS_FONT_SIZE: 17 + BACKGROUND_IMAGE: background.png + MEMORY: + GRAPH: + SHOW: true + X: 467 + Y: 416 + WIDTH: 117 + HEIGHT: 16 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 247, 227, 227 + BAR_OUTLINE: true + BACKGROUND_IMAGE: background.png + RADIAL: + SHOW: false + X: 517 + Y: 408 + RADIUS: 33 + WIDTH: 8 + MIN_VALUE: 0 + MAX_VALUE: 100 + ANGLE_START: 110 + ANGLE_END: 70 + ANGLE_STEPS: 1 + ANGLE_SEP: 25 + CLOCKWISE: True + BAR_COLOR: 247, 227, 227 + SHOW_TEXT: True + SHOW_UNIT: True + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 25 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.png + TEXT: + SHOW: true + SHOW_UNIT: True + X: 467 + Y: 392 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 25 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + TEMPERATURE: + TEXT: + SHOW: true + SHOW_UNIT: True + X: 583 + Y: 384 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 50 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + MEMORY: + INTERVAL: 1 + SWAP: + GRAPH: + SHOW: false + X: 192 + Y: 456 + WIDTH: 297 + HEIGHT: 21 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 255, 0, 0 + BAR_OUTLINE: False + BACKGROUND_IMAGE: background.png + RADIAL: + SHOW: False + X: 235 + Y: 440 + RADIUS: 47 + WIDTH: 13 + MIN_VALUE: 0 + MAX_VALUE: 100 + ANGLE_START: 110 + ANGLE_END: 70 + ANGLE_STEPS: 1 + ANGLE_SEP: 25 + CLOCKWISE: True + BAR_COLOR: 255, 0, 0 + SHOW_TEXT: True + SHOW_UNIT: True + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 22 + FONT_COLOR: 200, 200, 200 + BACKGROUND_IMAGE: background.png + VIRTUAL: + GRAPH: + SHOW: false + X: 67 + Y: 672 + WIDTH: 267 + HEIGHT: 24 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 247, 227, 227 + BAR_OUTLINE: true + BACKGROUND_IMAGE: background.png + LINE_GRAPH: + SHOW: True + X: 50 + Y: 608 + WIDTH: 300 + HEIGHT: 112 + MIN_VALUE: 0 + MAX_VALUE: 100 + HISTORY_SIZE: 200 + AUTOSCALE: False + LINE_COLOR: 247, 227, 227 + AXIS: true + AXIS_COLOR: 247, 227, 227 + AXIS_FONT: roboto/Roboto-Black.ttf + AXIS_FONT_SIZE: 17 + BACKGROUND_IMAGE: background.png + USED: + SHOW: True + SHOW_UNIT: false + X: 92 + Y: 560 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 37 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + FREE: + SHOW: FALSE + SHOW_UNIT: True + X: 303 + Y: 206 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + PERCENT_TEXT: + SHOW: True + SHOW_UNIT: True + X: 183 + Y: 496 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 58 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + DISK: + INTERVAL: 10 + USED: + GRAPH: + SHOW: true + X: 467 + Y: 640 + WIDTH: 267 + HEIGHT: 24 + MIN_VALUE: 0 + MAX_VALUE: 100 + BAR_COLOR: 247, 227, 227 + BAR_OUTLINE: true + BACKGROUND_IMAGE: background.png + RADIAL: + SHOW: false + X: 600 + Y: 664 + RADIUS: 58 + WIDTH: 10 + MIN_VALUE: 0 + MAX_VALUE: 100 + ANGLE_START: 120 + ANGLE_END: 70 + ANGLE_STEPS: 1 + ANGLE_SEP: 25 + CLOCKWISE: True + BAR_COLOR: 247, 227, 227 + SHOW_TEXT: True + SHOW_UNIT: True + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 42 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + TEXT: + SHOW: false + SHOW_UNIT: True + X: 340 + Y: 648 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + PERCENT_TEXT: + SHOW: true + SHOW_UNIT: True + X: 508 + Y: 576 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 67 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + TOTAL: + TEXT: + SHOW: False + SHOW_UNIT: True + X: 340 + Y: 600 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + FREE: + TEXT: + SHOW: False + SHOW_UNIT: True + X: 340 + Y: 696 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + NET: + INTERVAL: 1 + WLO: + UPLOAD: + TEXT: + SHOW: false + X: 545 + Y: 944 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 37 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + UPLOADED: + TEXT: + SHOW: false + X: 633 + Y: 992 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 28 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + DOWNLOAD: + TEXT: + SHOW: false + X: 145 + Y: 944 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 37 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + DOWNLOADED: + TEXT: + SHOW: false + X: 233 + Y: 992 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 28 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + ETH: + UPLOAD: + TEXT: + SHOW: true + X: 525 + Y: 864 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 37 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + LINE_GRAPH: + SHOW: true + X: 433 + Y: 960 + WIDTH: 300 + HEIGHT: 96 + MIN_VALUE: 0 + MAX_VALUE: 500000 + HISTORY_SIZE: 120 + AUTOSCALE: False + LINE_COLOR: 247, 227, 227 + AXIS: True + AXIS_COLOR: 247, 227, 227 + AXIS_FONT: roboto/Roboto-Black.ttf + AXIS_FONT_SIZE: 17 + BACKGROUND_IMAGE: background.png + UPLOADED: + TEXT: + SHOW: true + X: 617 + Y: 912 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 28 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + DOWNLOAD: + TEXT: + SHOW: true + X: 133 + Y: 864 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 37 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + LINE_GRAPH: + SHOW: true + X: 50 + Y: 960 + WIDTH: 300 + HEIGHT: 96 + MIN_VALUE: 0 + MAX_VALUE: 1500000 + HISTORY_SIZE: 120 + AUTOSCALE: False + LINE_COLOR: 247, 227, 227 + AXIS: True + AXIS_COLOR: 247, 227, 227 + AXIS_FONT: roboto/Roboto-Black.ttf + AXIS_FONT_SIZE: 17 + BACKGROUND_IMAGE: background.png + DOWNLOADED: + TEXT: + SHOW: true + X: 225 + Y: 912 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 28 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + DATE: + # For time display, it is recommended not to change the interval: keep to 1 + INTERVAL: 1 + DAY: # Format (Y/M/D ordering, month/day translations...) will match your computer locale + TEXT: + FORMAT: medium # short (2/20/23) / medium (Feb 20, 2023) / long (February 20, 2023) / full (Monday, February 20, 2023) + SHOW: True + X: 400 + Y: 40 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 50 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + WIDTH: 400 + ANCHOR: mt + HOUR: # Format (12/24h, timezone translations) will match your computer locale + TEXT: + FORMAT: medium # short (6:48 PM) / medium (6:48:53 PM) / long (6:48:53 PM UTC) / full (6:48:53 PM Coordinated Universal Time) + SHOW: True + X: 0 + Y: 40 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 50 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + WIDTH: 400 + ANCHOR: mt + WEATHER: + # For optimal use, if you don't want to trigger the free threshold daily call (1000 calls), the interval should be 90 MINIMUM (not really useful as the API didn't update that quickly) + INTERVAL: 300 + TEMPERATURE: + TEXT: + SHOW: True + X: 45 + Y: 112 + FONT: roboto-mono/RobotoMono-Bold.ttf + FONT_SIZE: 37 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + TEMPERATURE_FELT: + TEXT: + SHOW: True + X: 192 + Y: 109 + FONT: roboto-mono/RobotoMono-Bold.ttf + FONT_SIZE: 37 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + UPDATE_TIME: + TEXT: + SHOW: True + X: 617 + Y: 110 + FONT: roboto-mono/RobotoMono-Bold.ttf + FONT_SIZE: 37 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + HUMIDITY: + TEXT: + SHOW: True + X: 483 + Y: 110 + FONT: roboto-mono/RobotoMono-Bold.ttf + FONT_SIZE: 37 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + WEATHER_DESCRIPTION: + TEXT: + SHOW: True + X: 0 + Y: 152 + WIDTH: 800 + FONT: roboto-mono/RobotoMono-Bold.ttf + FONT_SIZE: 30 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + ANCHOR: mb # Text is centered + PING: + INTERVAL: 1 + TEXT: + SHOW: True + X: 542 + Y: 792 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 42 + FONT_COLOR: 247, 227, 227 + BACKGROUND_IMAGE: background.png + ALIGN: right + LINE_GRAPH: + SHOW: true + X: 50 + Y: 1088 + WIDTH: 692 + HEIGHT: 112 + MIN_VALUE: 0 + MAX_VALUE: 250 + HISTORY_SIZE: 120 + AUTOSCALE: false + LINE_COLOR: 247, 227, 227 + AXIS: True + AXIS_COLOR: 247, 227, 227 + AXIS_FONT: roboto/Roboto-Black.ttf + AXIS_FONT_SIZE: 17 + BACKGROUND_IMAGE: background.png diff --git a/res/themes/ColoredFlat_8inch/wallpaperv2.png b/res/themes/ColoredFlat_8inch/wallpaperv2.png new file mode 100644 index 00000000..bf2d7581 Binary files /dev/null and b/res/themes/ColoredFlat_8inch/wallpaperv2.png differ diff --git a/res/themes/Gradient46/background.png b/res/themes/Gradient46/background.png new file mode 100644 index 00000000..c5bea392 Binary files /dev/null and b/res/themes/Gradient46/background.png differ diff --git a/res/themes/Gradient46/preview.png b/res/themes/Gradient46/preview.png new file mode 100644 index 00000000..64411868 Binary files /dev/null and b/res/themes/Gradient46/preview.png differ diff --git a/res/themes/Gradient46/theme.yaml b/res/themes/Gradient46/theme.yaml new file mode 100644 index 00000000..b22b1b7b --- /dev/null +++ b/res/themes/Gradient46/theme.yaml @@ -0,0 +1,83 @@ +--- +author: "@mathoudebine" + +display: + DISPLAY_SIZE: 4.6" + DISPLAY_ORIENTATION: portrait + DISPLAY_RGB_LED: 226, 21, 103 + +static_images: + BACKGROUND: + PATH: background.png + X: 0 + Y: 0 + WIDTH: 320 + HEIGHT: 960 + +STATS: + CPU: + PERCENTAGE: + INTERVAL: 1 + TEXT: + SHOW: True + SHOW_UNIT: True + X: 40 + Y: 160 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 80 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + TEMPERATURE: + INTERVAL: 1 + TEXT: + SHOW: True + SHOW_UNIT: True + X: 80 + Y: 110 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 40 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + GPU: + INTERVAL: 1 + PERCENTAGE: + TEXT: + SHOW: True + SHOW_UNIT: True + X: 40 + Y: 480 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 80 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + TEMPERATURE: + TEXT: + SHOW: True + SHOW_UNIT: True + X: 80 + Y: 430 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 40 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + MEMORY: + INTERVAL: 5 + VIRTUAL: + USED: + SHOW: True + SHOW_UNIT: True + X: 80 + Y: 740 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 30 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png + PERCENT_TEXT: + SHOW: True + SHOW_UNIT: True + X: 40 + Y: 790 + FONT: roboto-mono/RobotoMono-Regular.ttf + FONT_SIZE: 80 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background.png diff --git a/res/themes/Landscape15Grid_8inch/background.png b/res/themes/Landscape15Grid_8inch/background.png new file mode 100644 index 00000000..e6c404b0 Binary files /dev/null and b/res/themes/Landscape15Grid_8inch/background.png differ diff --git a/res/themes/Landscape15Grid_8inch/background_grid.png b/res/themes/Landscape15Grid_8inch/background_grid.png new file mode 100644 index 00000000..57aec1cd Binary files /dev/null and b/res/themes/Landscape15Grid_8inch/background_grid.png differ diff --git a/res/themes/Landscape15Grid_8inch/grid.png b/res/themes/Landscape15Grid_8inch/grid.png new file mode 100644 index 00000000..b3439caa Binary files /dev/null and b/res/themes/Landscape15Grid_8inch/grid.png differ diff --git a/res/themes/Landscape15Grid_8inch/original_background.jpg b/res/themes/Landscape15Grid_8inch/original_background.jpg new file mode 100644 index 00000000..ab3330f9 Binary files /dev/null and b/res/themes/Landscape15Grid_8inch/original_background.jpg differ diff --git a/res/themes/Landscape15Grid_8inch/preview.png b/res/themes/Landscape15Grid_8inch/preview.png new file mode 100644 index 00000000..d9656f41 Binary files /dev/null and b/res/themes/Landscape15Grid_8inch/preview.png differ diff --git a/res/themes/Landscape15Grid_8inch/theme.yaml b/res/themes/Landscape15Grid_8inch/theme.yaml new file mode 100644 index 00000000..07510102 --- /dev/null +++ b/res/themes/Landscape15Grid_8inch/theme.yaml @@ -0,0 +1,307 @@ +--- +author: "@mathoudebine" + +display: + DISPLAY_SIZE: 8" + DISPLAY_ORIENTATION: landscape + DISPLAY_RGB_LED: 0, 0, 255 + +static_text: + CPULOAD: + TEXT: CPU Load + X: 61 + Y: 217 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + CPUTEMP: + TEXT: CPU Temp° + X: 301 + Y: 217 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + CPUCLOCK: + TEXT: CPU Clock + X: 550 + Y: 217 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + GPULOAD: + TEXT: GPU Load + X: 61 + Y: 467 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + GPUTEMP: + TEXT: GPU Temp° + X: 301 + Y: 467 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + GPURAM: + TEXT: GPU RAM + X: 557 + Y: 467 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + RAMLOAD: + TEXT: RAM Load + X: 797 + Y: 217 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + RAMFREE: + TEXT: RAM Free + X: 1053 + Y: 217 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + ETHDOWN: + TEXT: ETH Downl. + X: 790 + Y: 467 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + ETHUP: + TEXT: ETH Upload + X: 1037 + Y: 467 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + WLDOWN: + TEXT: WL Downl. + X: 797 + Y: 717 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + WLUP: + TEXT: WL Upload + X: 1046 + Y: 717 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + DISKLOAD: + TEXT: DISK Load + X: 304 + Y: 717 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + DISKFREE: + TEXT: DISK Free + X: 557 + Y: 717 + FONT: roboto/Roboto-Bold.ttf + FONT_SIZE: 38 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + +static_images: + BACKGROUND: + PATH: background_grid.png + X: 0 + Y: 0 + WIDTH: 1280 + HEIGHT: 800 + +STATS: + CPU: + PERCENTAGE: + INTERVAL: 1 + TEXT: + SHOW: True + SHOW_UNIT: True + X: 64 + Y: 110 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 64 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + FREQUENCY: + INTERVAL: 5 + TEXT: + SHOW: True + SHOW_UNIT: False + X: 576 + Y: 110 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 64 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + TEMPERATURE: + INTERVAL: 5 + TEXT: + SHOW: True + SHOW_UNIT: True + X: 296 + Y: 110 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 64 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + GPU: + INTERVAL: 1 + PERCENTAGE: + TEXT: + SHOW: True + SHOW_UNIT: True + X: 64 + Y: 352 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 64 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + TEMPERATURE: + TEXT: + SHOW: True + SHOW_UNIT: True + X: 296 + Y: 352 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 64 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + MEMORY: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 544 + Y: 352 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 64 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + MEMORY: + INTERVAL: 5 + VIRTUAL: + PERCENT_TEXT: + SHOW: True + SHOW_UNIT: True + X: 816 + Y: 110 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 64 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + FREE: + SHOW: True + SHOW_UNIT: False + X: 1043 + Y: 110 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 64 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + NET: + INTERVAL: 1 + WLO: + UPLOAD: + TEXT: + SHOW: True + X: 1034 + Y: 607 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + DOWNLOAD: + TEXT: + SHOW: True + X: 781 + Y: 607 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + ETH: + UPLOAD: + TEXT: + SHOW: True + X: 1034 + Y: 357 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + DOWNLOAD: + TEXT: + SHOW: True + X: 781 + Y: 357 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 35 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + DISK: + INTERVAL: 10 + USED: + PERCENT_TEXT: + SHOW: True + SHOW_UNIT: True + X: 320 + Y: 602 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 64 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + FREE: + TEXT: + SHOW: True + SHOW_UNIT: False + X: 544 + Y: 602 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 64 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + DATE: + # For time display, it is recommended not to change the interval: keep to 1 + INTERVAL: 1 + DAY: # Format (Y/M/D ordering, month/day translations...) will match your computer locale + TEXT: + FORMAT: short # short (2/20/23) / medium (Feb 20, 2023) / long (February 20, 2023) / full (Monday, February 20, 2023) + SHOW: True + X: 46 + Y: 717 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 32 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + WIDTH: 208 + ANCHOR: lt + HOUR: # Format (12/24h, timezone translations) will match your computer locale + TEXT: + FORMAT: short # short (6:48 PM) / medium (6:48:53 PM) / long (6:48:53 PM UTC) / full (6:48:53 PM Coordinated Universal Time) + SHOW: True + X: 46 + Y: 613 + FONT: jetbrains-mono/JetBrainsMono-Bold.ttf + FONT_SIZE: 45 + FONT_COLOR: 255, 255, 255 + BACKGROUND_IMAGE: background_grid.png + WIDTH: 208 + ANCHOR: lt diff --git a/res/themes/scale_theme.py b/res/themes/scale_theme.py new file mode 100644 index 00000000..dcd141fa --- /dev/null +++ b/res/themes/scale_theme.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-3.0-or-later +# +# 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 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 . + +""" +scale_theme.py — Rescale a turing-smart-screen theme YAML to a new resolution. + +Usage: + python scale_theme.py theme.yaml output.yaml --from 480x800 --to 800x1280 + python scale_theme.py theme.yaml output.yaml --from 800x480 --to 1280x800 +""" + +import sys +import re +import argparse +from pathlib import Path + +# --------------------------------------------------------------------------- +# Keys that hold X / Y coordinates or dimensions to rescale +# --------------------------------------------------------------------------- +X_KEYS = {"X", "RADIUS"} # scaled by x_factor +Y_KEYS = {"Y"} # scaled by y_factor +W_KEYS = {"WIDTH"} # scaled by x_factor +H_KEYS = {"HEIGHT"} # scaled by y_factor +FS_KEYS = {"FONT_SIZE", "AXIS_FONT_SIZE"} # scaled by avg factor + + +def parse_resolution(res: str) -> tuple[int, int]: + """Parse 'WxH' string into (width, height).""" + match = re.fullmatch(r"(\d+)[xX×](\d+)", res.strip()) + if not match: + raise ValueError(f"Invalid resolution format '{res}'. Expected WxH (e.g. 800x480).") + return int(match.group(1)), int(match.group(2)) + + +def scale_value(value: int | float, factor: float) -> int: + """Apply factor and round to nearest integer.""" + return round(value * factor) + + +def process_lines(lines: list[str], fx: float, fy: float) -> list[str]: + """ + Walk through YAML lines and rescale numeric values for known keys. + Handles both `KEY: value` and `WIDTH: value` forms. + Preserves comments, indentation, and all other content exactly. + """ + favg = (fx + fy) / 2 + result = [] + + for line in lines: + # Match lines like: [indent]KEY: number[optional comment] + m = re.match(r"^(\s*)(\w+)(\s*:\s*)(-?\d+(?:\.\d+)?)(.*)", line) + if m: + indent, key, sep, raw_val, rest = m.groups() + val = float(raw_val) + + if key in X_KEYS: + new_val = scale_value(val, fx) + elif key in Y_KEYS: + new_val = scale_value(val, fy) + elif key in W_KEYS: + new_val = scale_value(val, fx) + elif key in H_KEYS: + new_val = scale_value(val, fy) + elif key in FS_KEYS: + new_val = scale_value(val, favg) + else: + result.append(line) + continue + + # Preserve original int/float representation + if "." in raw_val: + result.append(f"{indent}{key}{sep}{float(new_val)}{rest}\n") + else: + result.append(f"{indent}{key}{sep}{new_val}{rest}\n") + else: + # Handle PATH / background dims in static_images block: + # WIDTH: 800 → already caught above + # Also handle DISPLAY_SIZE (ignored as per spec): + result.append(line) + + return result + + +def scale_theme(src: Path, dst: Path, src_res: str, dst_res: str) -> None: + sw, sh = parse_resolution(src_res) + dw, dh = parse_resolution(dst_res) + + fx = dw / sw + fy = dh / sh + + print(f"Source : {src}") + print(f"Destination : {dst}") + print(f"From : {sw}×{sh}") + print(f"To : {dw}×{dh}") + print(f"X factor : {fx:.6f} ({sw} → {dw})") + print(f"Y factor : {fy:.6f} ({sh} → {dh})") + print(f"Font factor : {(fx+fy)/2:.6f} (average)") + + lines = src.read_text(encoding="utf-8").splitlines(keepends=True) + scaled = process_lines(lines, fx, fy) + dst.write_text("".join(scaled), encoding="utf-8") + + print(f"\nDone — written to {dst}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Rescale a turing-smart-screen theme YAML to a new resolution." + ) + parser.add_argument("input", type=Path, help="Source theme.yaml") + parser.add_argument("output", type=Path, help="Output theme.yaml") + parser.add_argument("--from", dest="src_res", required=True, + metavar="WxH", help="Original resolution, e.g. 480x800") + parser.add_argument("--to", dest="dst_res", required=True, + metavar="WxH", help="Target resolution, e.g. 800x1280") + args = parser.parse_args() + + if not args.input.exists(): + print(f"Error: file not found: {args.input}", file=sys.stderr) + sys.exit(1) + + scale_theme(args.input, args.output, args.src_res, args.dst_res) + + +if __name__ == "__main__": + main() diff --git a/simple-program.py b/simple-program.py index 287c92d1..501cf931 100755 --- a/simple-program.py +++ b/simple-program.py @@ -35,31 +35,24 @@ 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_comm_turing_usb import LcdCommTuringUSB from library.lcd.lcd_comm_weact_a import LcdCommWeActA from library.lcd.lcd_comm_weact_b import LcdCommWeActB 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 +# Set your COM port e.g. COM3 for Windows, /dev/ttyACM0 for Linux, etc. +# Use AUTO for COM port auto-discovery (may not work on every setup) or if device is not detected as a COM port # COM_PORT = "/dev/ttyACM0" # COM_PORT = "COM5" COM_PORT = "AUTO" -# 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 simulated display (image written in screencap.png) +# Display revision: see config.yaml comments for values # To identify your smart screen: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions REVISION = "A" # Display width & height in pixels for portrait orientation # /!\ Do not switch width/height here for landscape, use lcd_comm.SetOrientation below -# 320x480 for 3.5" models -# 480x480 for 2.1" models -# 480x800 for 5" models -# 480x1920 for 8.8" models WIDTH, HEIGHT = 320, 480 assert WIDTH <= HEIGHT, "Indicate display width/height for PORTRAIT orientation: width <= height" @@ -84,7 +77,6 @@ def sighandler(signum, frame): lcd_comm = None if REVISION == "A": logger.info("Selected Hardware Revision A (Turing Smart Screen 3.5\" & UsbPCMonitor 3.5\"/5\")") - # NOTE: If you have UsbPCMonitor 5" you need to change the width/height to 480x800 below lcd_comm = LcdCommRevA(com_port=COM_PORT, display_width=WIDTH, display_height=HEIGHT) elif REVISION == "B": logger.info("Selected Hardware Revision B (XuanFang screen 3.5\" version B / flagship)") @@ -95,6 +87,9 @@ def sighandler(signum, frame): elif REVISION == "D": logger.info("Selected Hardware Revision D (Kipye Qiye Smart Display 3.5\")") lcd_comm = LcdCommRevD(com_port=COM_PORT, display_width=WIDTH, display_height=HEIGHT) + elif REVISION == "TUR_USB": + logger.info("Selected Hardware Revision Turing USB (newer models 4.6\"/5.2\"/8\"/8.8\" HW rev 1.x/9.2\"") + lcd_comm = LcdCommTuringUSB(com_port=COM_PORT, display_width=WIDTH, display_height=HEIGHT) elif REVISION == "WEACT_A": logger.info("Selected Hardware WeAct Studio Display FS V1 3.5\"") lcd_comm = LcdCommWeActA(com_port=COM_PORT, display_width=WIDTH, display_height=HEIGHT) diff --git a/tools/turing-theme-extractor.py b/tools/turing-theme-extractor.py index 9d44df3c..c638ebb3 100644 --- a/tools/turing-theme-extractor.py +++ b/tools/turing-theme-extractor.py @@ -22,9 +22,12 @@ # turing-theme-extractor.py: Extract resources from a Turing Smart Screen theme (.data files) made for Windows app # This program will search and extract PNGs from the theme data and extract theme in the current directory # The PNG can then be re-used to create a theme for System Monitor python program (see Wiki for theme creation) +# To run for all files of a folder: +# for file in "FOLDER"/*; do if [ -f "$file" ]; then python turing-theme-extractor.py "$file"; fi; done import mmap import os import sys +from pathlib import Path PNG_SIGNATURE = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A' PNG_IEND = b'\x49\x45\x4E\x44\xAE\x42\x60\x82' @@ -46,6 +49,11 @@ with open(sys.argv[1], "r+b") as theme_file: mm = mmap.mmap(theme_file.fileno(), 0) + theme_name = Path(sys.argv[1]).stem + + if not os.path.isdir(theme_name): + os.makedirs(theme_name) + # Find PNG signature in binary data start_pos=0 header_found = mm.find(PNG_SIGNATURE, 0) @@ -59,15 +67,14 @@ # Extract PNG data to a file theme_file.seek(header_found) - png_file = open('theme_res_' + str(header_found) + '.png', 'wb') + png_file = open(theme_name + "/" + str(header_found) + '.png', 'wb') png_file.write(theme_file.read(iend_found - header_found + len(PNG_IEND))) png_file.close() - print("PNG extracted to theme_res_%s.png" % str(header_found)) + print("PNG extracted to %s/%s.png" % (theme_name, str(header_found))) found_png = found_png + 1 # Find next PNG signature (if any) header_found = mm.find(PNG_SIGNATURE, iend_found) print("\n%d PNG files extracted from theme to current directory" % found_png) -