Skip to content

Commit f6521f0

Browse files
authored
Merge pull request #177 from mathoudebine/feature/176-add-a-configuration-gui
Feature/176 add a configuration gui
2 parents 569686d + 0043a6e commit f6521f0

File tree

10 files changed

+361
-20
lines changed

10 files changed

+361
-20
lines changed

configure.py

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
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()

library/lcd/lcd_comm_rev_a.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def __del__(self):
4747

4848
@staticmethod
4949
def auto_detect_com_port():
50-
com_ports = serial.tools.list_ports.comports()
50+
com_ports = comports()
5151
auto_com_port = None
5252

5353
for com_port in com_ports:

library/lcd/lcd_comm_rev_b.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def is_brightness_range(self):
6565

6666
@staticmethod
6767
def auto_detect_com_port():
68-
com_ports = serial.tools.list_ports.comports()
68+
com_ports = comports()
6969
auto_com_port = None
7070

7171
for com_port in com_ports:

library/log.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@
1717
# along with this program. If not, see <https://www.gnu.org/licenses/>.
1818

1919
# Configure logging format
20+
import locale
2021
import logging
21-
from datetime import datetime
22-
date = datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
22+
from logging.handlers import RotatingFileHandler
2323

24-
logging.basicConfig(# format='%(asctime)s [%(levelname)s] %(message)s in %(pathname)s:%(lineno)d',
25-
format="%(asctime)s [%(levelname)s] %(message)s",
26-
handlers=[
27-
# logging.FileHandler("log_"+date+".log", mode='w'), # Log in textfile
28-
logging.StreamHandler() # Log also in console
29-
],
30-
datefmt='%H:%M:%S')
24+
# use current locale for date/time formatting in logs
25+
locale.setlocale(locale.LC_ALL, '')
26+
27+
logging.basicConfig( # format='%(asctime)s [%(levelname)s] %(message)s in %(pathname)s:%(lineno)d',
28+
format="%(asctime)s [%(levelname)s] %(message)s",
29+
handlers=[
30+
RotatingFileHandler("log.log", maxBytes=1000000, backupCount=0), # Log in textfile max 1MB
31+
logging.StreamHandler() # Log also in console
32+
],
33+
datefmt='%x %X')
3134

3235
logger = logging.getLogger('turing')
3336
logger.setLevel(logging.DEBUG) # Lowest log level : print all messages

0 commit comments

Comments
 (0)