|
| 1 | +#!/usr/bin/env python |
| 2 | +# turing-smart-screen-python - a Python system monitor and library for 3.5" USB-C displays like Turing Smart Screen or XuanFang |
| 3 | +# https://github.com/mathoudebine/turing-smart-screen-python/ |
| 4 | + |
| 5 | +# Copyright (C) 2021-2023 Matthieu Houdebine (mathoudebine) |
| 6 | +# |
| 7 | +# This program is free software: you can redistribute it and/or modify |
| 8 | +# it under the terms of the GNU General Public License as published by |
| 9 | +# the Free Software Foundation, either version 3 of the License, or |
| 10 | +# (at your option) any later version. |
| 11 | +# |
| 12 | +# This program is distributed in the hope that it will be useful, |
| 13 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | +# GNU General Public License for more details. |
| 16 | +# |
| 17 | +# You should have received a copy of the GNU General Public License |
| 18 | +# along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 19 | + |
| 20 | +# This file is the system monitor configuration GUI |
| 21 | + |
| 22 | +import os |
| 23 | +import subprocess |
| 24 | +import sys |
| 25 | +import tkinter.ttk as ttk |
| 26 | +from tkinter import * |
| 27 | + |
| 28 | +import psutil |
| 29 | +import ruamel.yaml |
| 30 | +import sv_ttk |
| 31 | +from PIL import Image, ImageTk |
| 32 | +from serial.tools.list_ports import comports |
| 33 | + |
| 34 | +# Maps between config.yaml values and GUI description |
| 35 | +revision_map = {'A': "Turing / rev. A", 'B': "XuanFang / rev. B / flagship", 'SIMU': "Simulated screen"} |
| 36 | +hw_lib_map = {"AUTO": "Automatic", "LHM": "LibreHardwareMonitor (admin.)", "PYTHON": "Python libraries", |
| 37 | + "STUB": "Fake random data", "STATIC": "Fake static data"} |
| 38 | +reverse_map = {False: "classic", True: "reverse"} |
| 39 | + |
| 40 | + |
| 41 | +def get_themes(): |
| 42 | + themes = [] |
| 43 | + directory = 'res/themes/' |
| 44 | + for filename in os.listdir('res/themes'): |
| 45 | + f = os.path.join(directory, filename) |
| 46 | + # checking if it is a file |
| 47 | + if os.path.isdir(f): |
| 48 | + theme = os.path.join(f, 'theme.yaml') |
| 49 | + if os.path.isfile(theme): |
| 50 | + themes.append(filename) |
| 51 | + return sorted(themes, key=str.casefold) |
| 52 | + |
| 53 | + |
| 54 | +def get_com_ports(): |
| 55 | + com_ports_names = ["Automatic detection"] # Add manual entry on top for automatic detection |
| 56 | + com_ports = comports() |
| 57 | + for com_port in com_ports: |
| 58 | + com_ports_names.append(com_port.name) |
| 59 | + return com_ports_names |
| 60 | + |
| 61 | + |
| 62 | +def get_net_if(): |
| 63 | + if_list = list(psutil.net_if_addrs().keys()) |
| 64 | + if_list.insert(0, "None") # Add manual entry on top for unavailable/not selected interface |
| 65 | + return if_list |
| 66 | + |
| 67 | + |
| 68 | +class TuringConfigWindow: |
| 69 | + def __init__(self): |
| 70 | + self.window = Tk() |
| 71 | + self.window.title('Turing System Monitor configuration') |
| 72 | + self.window.geometry("730x510") |
| 73 | + self.window.iconphoto(True, PhotoImage(file="res/icons/monitor-icon-17865/64.png")) |
| 74 | + # When window gets focus again, reload theme preview in case it has been updated by theme editor |
| 75 | + self.window.bind("<FocusIn>", self.on_theme_change) |
| 76 | + |
| 77 | + # Make TK look better with Sun Valley ttk theme |
| 78 | + sv_ttk.set_theme("light") |
| 79 | + |
| 80 | + self.theme_preview_img = None |
| 81 | + self.theme_preview = ttk.Label(self.window) |
| 82 | + self.theme_preview.place(x=10, y=10) |
| 83 | + |
| 84 | + sysmon_label = ttk.Label(self.window, text='System Monitor configuration', font='bold') |
| 85 | + sysmon_label.place(x=320, y=0) |
| 86 | + |
| 87 | + self.theme_label = ttk.Label(self.window, text='Theme') |
| 88 | + self.theme_label.place(x=320, y=35) |
| 89 | + self.theme_cb = ttk.Combobox(self.window, values=get_themes(), state='readonly') |
| 90 | + self.theme_cb.place(x=500, y=30, width=210) |
| 91 | + self.theme_cb.bind('<<ComboboxSelected>>', self.on_theme_change) |
| 92 | + |
| 93 | + self.hwlib_label = ttk.Label(self.window, text='Hardware monitoring') |
| 94 | + self.hwlib_label.place(x=320, y=75) |
| 95 | + if sys.platform != "win32": |
| 96 | + del hw_lib_map["LHM"] # LHM is for Windows platforms only |
| 97 | + self.hwlib_cb = ttk.Combobox(self.window, values=list(hw_lib_map.values()), state='readonly') |
| 98 | + self.hwlib_cb.place(x=500, y=70, width=210) |
| 99 | + self.hwlib_cb.bind('<<ComboboxSelected>>', self.on_hwlib_change) |
| 100 | + |
| 101 | + self.eth_label = ttk.Label(self.window, text='Ethernet interface') |
| 102 | + self.eth_label.place(x=320, y=115) |
| 103 | + self.eth_cb = ttk.Combobox(self.window, values=get_net_if(), state='readonly') |
| 104 | + self.eth_cb.place(x=500, y=110, width=210) |
| 105 | + |
| 106 | + self.wl_label = ttk.Label(self.window, text='Wi-Fi interface') |
| 107 | + self.wl_label.place(x=320, y=155) |
| 108 | + self.wl_cb = ttk.Combobox(self.window, values=get_net_if(), state='readonly') |
| 109 | + self.wl_cb.place(x=500, y=150, width=210) |
| 110 | + |
| 111 | + self.lhm_admin_warning = ttk.Label(self.window, |
| 112 | + text="Admin rights needed, or select another Hardware monitoring", |
| 113 | + foreground='#00f') |
| 114 | + self.lhm_admin_warning.place(x=320, y=190) |
| 115 | + |
| 116 | + sysmon_label = ttk.Label(self.window, text='Display configuration', font='bold') |
| 117 | + sysmon_label.place(x=320, y=220) |
| 118 | + |
| 119 | + self.model_label = ttk.Label(self.window, text='Smart screen model') |
| 120 | + self.model_label.place(x=320, y=265) |
| 121 | + self.model_cb = ttk.Combobox(self.window, values=list(revision_map.values()), state='readonly') |
| 122 | + self.model_cb.bind('<<ComboboxSelected>>', self.on_model_change) |
| 123 | + self.model_cb.place(x=500, y=260, width=210) |
| 124 | + |
| 125 | + self.com_label = ttk.Label(self.window, text='COM port') |
| 126 | + self.com_label.place(x=320, y=305) |
| 127 | + self.com_cb = ttk.Combobox(self.window, values=get_com_ports(), state='readonly') |
| 128 | + self.com_cb.place(x=500, y=300, width=210) |
| 129 | + |
| 130 | + self.orient_label = ttk.Label(self.window, text='Orientation') |
| 131 | + self.orient_label.place(x=320, y=345) |
| 132 | + self.orient_cb = ttk.Combobox(self.window, values=list(reverse_map.values()), state='readonly') |
| 133 | + self.orient_cb.place(x=500, y=340, width=210) |
| 134 | + |
| 135 | + self.brightness_string = StringVar() |
| 136 | + self.brightness_label = ttk.Label(self.window, text='Brightness') |
| 137 | + self.brightness_label.place(x=320, y=385) |
| 138 | + self.brightness_slider = ttk.Scale(self.window, from_=0, to=100, orient=HORIZONTAL, |
| 139 | + command=self.on_brightness_change) |
| 140 | + self.brightness_slider.place(x=550, y=380, width=160) |
| 141 | + self.brightness_val_label = ttk.Label(self.window, textvariable=self.brightness_string) |
| 142 | + self.brightness_val_label.place(x=500, y=385) |
| 143 | + self.brightness_warning_label = ttk.Label(self.window, |
| 144 | + text="⚠ Turing / rev. A displays can get hot at high brightness!", |
| 145 | + foreground='#f00') |
| 146 | + self.brightness_warning_label.place(x=320, y=420) |
| 147 | + |
| 148 | + self.edit_theme_btn = ttk.Button(self.window, text="Edit theme", command=lambda: self.on_theme_editor_click()) |
| 149 | + self.edit_theme_btn.place(x=310, y=450, height=50, width=130) |
| 150 | + |
| 151 | + self.save_btn = ttk.Button(self.window, text="Save settings", command=lambda: self.on_save_click()) |
| 152 | + self.save_btn.place(x=450, y=450, height=50, width=130) |
| 153 | + |
| 154 | + self.save_run_btn = ttk.Button(self.window, text="Save and run", command=lambda: self.on_saverun_click()) |
| 155 | + self.save_run_btn.place(x=590, y=450, height=50, width=130) |
| 156 | + |
| 157 | + self.config = None |
| 158 | + self.load_config_values() |
| 159 | + |
| 160 | + def run(self): |
| 161 | + self.window.mainloop() |
| 162 | + |
| 163 | + def load_theme_preview(self): |
| 164 | + try: |
| 165 | + theme_preview = Image.open("res/themes/" + self.theme_cb.get() + "/preview.png") |
| 166 | + except: |
| 167 | + theme_preview = Image.open("res/docs/no-preview.png") |
| 168 | + finally: |
| 169 | + if theme_preview.width > theme_preview.height: |
| 170 | + theme_preview = theme_preview.resize((300, 200), Image.Resampling.LANCZOS) |
| 171 | + else: |
| 172 | + theme_preview = theme_preview.resize((280, 420), Image.Resampling.LANCZOS) |
| 173 | + self.theme_preview_img = ImageTk.PhotoImage(theme_preview) |
| 174 | + self.theme_preview.config(image=self.theme_preview_img) |
| 175 | + |
| 176 | + def load_config_values(self): |
| 177 | + with open("config.yaml", "rt", encoding='utf8') as stream: |
| 178 | + self.config, ind, bsi = ruamel.yaml.util.load_yaml_guess_indent(stream) |
| 179 | + |
| 180 | + try: |
| 181 | + self.theme_cb.set(self.config['config']['THEME']) |
| 182 | + except: |
| 183 | + self.theme_cb.current(0) |
| 184 | + self.load_theme_preview() |
| 185 | + |
| 186 | + try: |
| 187 | + self.hwlib_cb.set(hw_lib_map[self.config['config']['HW_SENSORS']]) |
| 188 | + except: |
| 189 | + self.hwlib_cb.current(0) |
| 190 | + |
| 191 | + try: |
| 192 | + if self.config['config']['ETH'] == "": |
| 193 | + self.eth_cb.current(0) |
| 194 | + else: |
| 195 | + self.eth_cb.set(self.config['config']['ETH']) |
| 196 | + except: |
| 197 | + self.eth_cb.current(0) |
| 198 | + |
| 199 | + try: |
| 200 | + if self.config['config']['WLO'] == "": |
| 201 | + self.wl_cb.current(0) |
| 202 | + else: |
| 203 | + self.wl_cb.set(self.config['config']['WLO']) |
| 204 | + except: |
| 205 | + self.wl_cb.current(0) |
| 206 | + |
| 207 | + try: |
| 208 | + if self.config['config']['COM_PORT'] == "AUTO": |
| 209 | + self.com_cb.current(0) |
| 210 | + else: |
| 211 | + self.com_cb.set(self.config['config']['COM_PORT']) |
| 212 | + except: |
| 213 | + self.com_cb.current(0) |
| 214 | + |
| 215 | + try: |
| 216 | + self.model_cb.set(revision_map[self.config['display']['REVISION']]) |
| 217 | + except: |
| 218 | + self.model_cb.current(0) |
| 219 | + |
| 220 | + try: |
| 221 | + self.orient_cb.set(reverse_map[self.config['display']['DISPLAY_REVERSE']]) |
| 222 | + except: |
| 223 | + self.orient_cb.current(0) |
| 224 | + |
| 225 | + try: |
| 226 | + self.brightness_slider.set(int(self.config['display']['BRIGHTNESS'])) |
| 227 | + except: |
| 228 | + self.brightness_slider.set(50) |
| 229 | + |
| 230 | + # Reload content on screen |
| 231 | + self.on_model_change() |
| 232 | + self.on_theme_change() |
| 233 | + self.on_brightness_change() |
| 234 | + self.on_hwlib_change() |
| 235 | + |
| 236 | + def save_config_values(self): |
| 237 | + self.config['config']['THEME'] = self.theme_cb.get() |
| 238 | + self.config['config']['HW_SENSORS'] = [k for k, v in hw_lib_map.items() if v == self.hwlib_cb.get()][0] |
| 239 | + if self.eth_cb.current() == 0: |
| 240 | + self.config['config']['ETH'] = "" |
| 241 | + else: |
| 242 | + self.config['config']['ETH'] = self.eth_cb.get() |
| 243 | + if self.wl_cb.current() == 0: |
| 244 | + self.config['config']['WLO'] = "" |
| 245 | + else: |
| 246 | + self.config['config']['WLO'] = self.wl_cb.get() |
| 247 | + if self.com_cb.current() == 0: |
| 248 | + self.config['config']['COM_PORT'] = "AUTO" |
| 249 | + else: |
| 250 | + self.config['config']['COM_PORT'] = self.com_cb.get() |
| 251 | + self.config['display']['REVISION'] = [k for k, v in revision_map.items() if v == self.model_cb.get()][0] |
| 252 | + self.config['display']['DISPLAY_REVERSE'] = [k for k, v in reverse_map.items() if v == self.orient_cb.get()][0] |
| 253 | + self.config['display']['BRIGHTNESS'] = int(self.brightness_slider.get()) |
| 254 | + |
| 255 | + with open("config.yaml", "w", encoding='utf-8') as file: |
| 256 | + ruamel.yaml.YAML().dump(self.config, file) |
| 257 | + |
| 258 | + def on_theme_change(self, e=None): |
| 259 | + self.load_theme_preview() |
| 260 | + |
| 261 | + def on_theme_editor_click(self): |
| 262 | + subprocess.Popen(os.path.join(os.getcwd(), "theme-editor.py") + " " + self.theme_cb.get(), shell=True) |
| 263 | + |
| 264 | + def on_save_click(self): |
| 265 | + self.save_config_values() |
| 266 | + |
| 267 | + def on_saverun_click(self): |
| 268 | + self.save_config_values() |
| 269 | + subprocess.Popen(os.path.join(os.getcwd(), "main.py"), shell=True) |
| 270 | + self.window.destroy() |
| 271 | + |
| 272 | + def on_brightness_change(self, e=None): |
| 273 | + self.brightness_string.set(str(int(self.brightness_slider.get())) + "%") |
| 274 | + self.show_hide_brightness_warning() |
| 275 | + |
| 276 | + def on_model_change(self, e=None): |
| 277 | + self.show_hide_brightness_warning() |
| 278 | + if [k for k, v in revision_map.items() if v == self.model_cb.get()][0] == "SIMU": |
| 279 | + self.com_cb.configure(state="disabled", foreground="#C0C0C0") |
| 280 | + self.orient_cb.configure(state="disabled", foreground="#C0C0C0") |
| 281 | + self.brightness_slider.configure(state="disabled") |
| 282 | + self.brightness_val_label.configure(foreground="#C0C0C0") |
| 283 | + else: |
| 284 | + self.com_cb.configure(state="readonly", foreground="#000") |
| 285 | + self.orient_cb.configure(state="readonly", foreground="#000") |
| 286 | + self.brightness_slider.configure(state="normal") |
| 287 | + self.brightness_val_label.configure(foreground="#000") |
| 288 | + |
| 289 | + def on_hwlib_change(self, e=None): |
| 290 | + hwlib = [k for k, v in hw_lib_map.items() if v == self.hwlib_cb.get()][0] |
| 291 | + if hwlib == "STUB" or hwlib == "STATIC": |
| 292 | + self.eth_cb.configure(state="disabled", foreground="#C0C0C0") |
| 293 | + self.wl_cb.configure(state="disabled", foreground="#C0C0C0") |
| 294 | + else: |
| 295 | + self.eth_cb.configure(state="readonly", foreground="#000") |
| 296 | + self.wl_cb.configure(state="readonly", foreground="#000") |
| 297 | + |
| 298 | + if hwlib == "LHM": |
| 299 | + self.lhm_admin_warning.place(x=320, y=190) |
| 300 | + elif hwlib == "AUTO" and sys.platform == "win32": |
| 301 | + self.lhm_admin_warning.place(x=320, y=190) |
| 302 | + else: |
| 303 | + self.lhm_admin_warning.place_forget() |
| 304 | + |
| 305 | + def show_hide_brightness_warning(self, e=None): |
| 306 | + if int(self.brightness_slider.get()) > 50 and [k for k, v in revision_map.items() if v == self.model_cb.get()][ |
| 307 | + 0] == "A": |
| 308 | + # Show warning for Turing Smart screen with high brightness |
| 309 | + self.brightness_warning_label.place(x=320, y=420) |
| 310 | + else: |
| 311 | + self.brightness_warning_label.place_forget() |
| 312 | + |
| 313 | + |
| 314 | +if __name__ == "__main__": |
| 315 | + configurator = TuringConfigWindow() |
| 316 | + configurator.run() |
0 commit comments