Skip to content

Commit 3a0d441

Browse files
authored
Merge pull request #149 from mathoudebine/feature/theme-editor
Theme Editor with preview window
2 parents c1c40f1 + b87d564 commit 3a0d441

File tree

17 files changed

+814
-512
lines changed

17 files changed

+814
-512
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
name: System monitor - themes screenshot
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- 'releases/**'
8+
pull_request:
9+
10+
jobs:
11+
system-monitor-theme-screenshot:
12+
13+
runs-on: ubuntu-latest
14+
15+
strategy:
16+
fail-fast: false
17+
18+
steps:
19+
- uses: actions/checkout@v3
20+
- name: Set up Python 3.10
21+
uses: actions/setup-python@v4
22+
with:
23+
python-version: "3.10"
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
python -m pip install -r requirements.txt
29+
30+
- name: Configure system monitor for screenshot
31+
run: |
32+
# For tests there is no real HW: use simulated LCD mode
33+
sed -i "/REVISION:/c\ REVISION: SIMU" config.yaml
34+
35+
# Use static data
36+
sed -i "/HW_SENSORS:/c\ HW_SENSORS: STATIC" config.yaml
37+
38+
- name: Run system monitor for 5 seconds on all themes
39+
run: |
40+
for dir in res/themes/*/
41+
do
42+
# Keep only folder name
43+
theme=`basename ${dir%*/}`
44+
45+
# Setup selected theme in config.yaml
46+
echo "Using theme $theme"
47+
sed -i "/THEME:/c\ THEME: $theme" config.yaml
48+
49+
# Run system-monitor for 5 seconds
50+
python3 main.py > output.log 2>&1 &
51+
sleep 5
52+
killall -9 python3
53+
54+
# Rename screen capture
55+
cp screencap.png screenshot-$theme.png
56+
done
57+
58+
- name: Archive screenshots
59+
uses: actions/upload-artifact@v3
60+
with:
61+
name: themes-screenshots
62+
path: screenshot-*.png

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,15 @@ Some themes are already included for a quick start!
4949
* Display configuration using `config.yaml` file: no Python code to edit.
5050
* Support for all [3.5" smart screen models (Turing and XuanFang)](https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions). Backplate RGB LEDs are also supported for available models!
5151
* 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.
52-
* Allow [creation of themes (see `res/themes`) with `theme.yaml` files](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)
52+
* 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)
5353
* Easy to expand: additional code that pulls specific information can be written in a modular way without impacting existing code.
5454
* Auto detect comm port. No longer need to hard set it, or if it changes on you then the config is wrong.
5555
* Tray icon with Exit option, useful when the program is running in background
5656

5757
Screenshots from the latest version using included themes (click on the thumbnails to see a bigger preview):
5858
<img src="res/docs/Theme3.5Inch.jpg" height="300" /> <img src="res/docs/ThemeTerminal.jpg" height="300" /> <img src="res/docs/ThemeCyberpunk.png" height="300" /> <img src="res/docs/ThemeBashDarkGreenGpu.png" height="300" /> <img src="res/docs/ThemeLandscape6Grid.jpg" width="300" /> <img src="res/docs/ThemeLandscapeMagicBlue.png" width="300" />
5959

60-
### [> Themes creation/edition](https://github.com/mathoudebine/turing-smart-screen-python/wiki/System-monitor-:-themes)
60+
### [> Themes creation/edition (using theme editor)](https://github.com/mathoudebine/turing-smart-screen-python/wiki/System-monitor-:-themes)
6161
### [> Themes shared by the community](https://github.com/mathoudebine/turing-smart-screen-python/discussions/categories/themes)
6262

6363
## Control the display from your Python projects

library/config.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,29 +36,37 @@ def load_yaml(configfile):
3636
PATH = sys.path[0]
3737
CONFIG_DATA = load_yaml("config.yaml")
3838
THEME_DEFAULT = load_yaml("res/themes/default.yaml")
39+
THEME_DATA = None
3940

40-
try:
41-
theme_path = "res/themes/" + CONFIG_DATA['config']['THEME'] + "/"
42-
logger.info("Loading theme %s from %s" % (CONFIG_DATA['config']['THEME'], theme_path + "theme.yaml"))
43-
THEME_DATA = load_yaml(theme_path + "theme.yaml")
44-
THEME_DATA['PATH'] = theme_path
45-
except:
46-
logger.error("Theme not found or contains errors!")
47-
try:
48-
sys.exit(0)
49-
except:
50-
os._exit(0)
5141

5242
def copy_default(default, theme):
53-
"recursively supply default values into a dict of dicts of dicts ...."
54-
for k,v in default.items():
43+
"""recursively supply default values into a dict of dicts of dicts ...."""
44+
for k, v in default.items():
5545
if k not in theme:
5646
theme[k] = v
5747
if type(v) == type({}):
5848
copy_default(default[k], theme[k])
5949

6050

61-
copy_default(THEME_DEFAULT, THEME_DATA)
51+
def load_theme():
52+
global THEME_DATA
53+
try:
54+
theme_path = "res/themes/" + CONFIG_DATA['config']['THEME'] + "/"
55+
logger.info("Loading theme %s from %s" % (CONFIG_DATA['config']['THEME'], theme_path + "theme.yaml"))
56+
THEME_DATA = load_yaml(theme_path + "theme.yaml")
57+
THEME_DATA['PATH'] = theme_path
58+
except:
59+
logger.error("Theme not found or contains errors!")
60+
try:
61+
sys.exit(0)
62+
except:
63+
os._exit(0)
64+
65+
copy_default(THEME_DEFAULT, THEME_DATA)
66+
67+
68+
# Load theme on import
69+
load_theme()
6270

6371
# Queue containing the serial requests to send to the screen
6472
update_queue = queue.Queue()

library/display.py

Lines changed: 41 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@
2323
from library.lcd.lcd_simulated import LcdSimulated
2424
from library.log import logger
2525

26-
THEME_DATA = config.THEME_DATA
27-
CONFIG_DATA = config.CONFIG_DATA
28-
2926

3027
def _get_full_path(path, name):
3128
if name:
@@ -35,47 +32,48 @@ def _get_full_path(path, name):
3532

3633

3734
def _get_theme_orientation() -> Orientation:
38-
if THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'portrait':
39-
if CONFIG_DATA["display"].get("DISPLAY_REVERSE", False):
35+
if config.THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'portrait':
36+
if config.CONFIG_DATA["display"].get("DISPLAY_REVERSE", False):
4037
return Orientation.REVERSE_PORTRAIT
4138
else:
4239
return Orientation.PORTRAIT
43-
elif THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'landscape':
44-
if CONFIG_DATA["display"].get("DISPLAY_REVERSE", False):
40+
elif config.THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'landscape':
41+
if config.CONFIG_DATA["display"].get("DISPLAY_REVERSE", False):
4542
return Orientation.REVERSE_LANDSCAPE
4643
else:
4744
return Orientation.LANDSCAPE
48-
elif THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'reverse_portrait':
45+
elif config.THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'reverse_portrait':
4946
logger.warn("'reverse_portrait' is deprecated as DISPLAY_ORIENTATION value in the theme."
5047
"Use 'portrait' instead, and use DISPLAY_REVERSE in config.yaml to reverse orientation.")
5148
return Orientation.REVERSE_PORTRAIT
52-
elif THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'reverse_landscape':
49+
elif config.THEME_DATA["display"]["DISPLAY_ORIENTATION"] == 'reverse_landscape':
5350
logger.warn("'reverse_landscape' is deprecated as DISPLAY_ORIENTATION value in the theme."
5451
"Use 'landscape' instead, and use DISPLAY_REVERSE in config.yaml to reverse orientation.")
5552
return Orientation.REVERSE_LANDSCAPE
5653
else:
57-
logger.warning("Orientation '", THEME_DATA["display"]["DISPLAY_ORIENTATION"], "' unknown, using portrait")
54+
logger.warning("Orientation '", config.THEME_DATA["display"]["DISPLAY_ORIENTATION"],
55+
"' unknown, using portrait")
5856
return Orientation.PORTRAIT
5957

6058

6159
class Display:
6260
def __init__(self):
6361
self.lcd = None
64-
if CONFIG_DATA["display"]["REVISION"] == "A":
65-
self.lcd = LcdCommRevA(com_port=CONFIG_DATA['config']['COM_PORT'],
66-
display_width=CONFIG_DATA["display"]["DISPLAY_WIDTH"],
67-
display_height=CONFIG_DATA["display"]["DISPLAY_HEIGHT"],
62+
if config.CONFIG_DATA["display"]["REVISION"] == "A":
63+
self.lcd = LcdCommRevA(com_port=config.CONFIG_DATA['config']['COM_PORT'],
64+
display_width=config.CONFIG_DATA["display"]["DISPLAY_WIDTH"],
65+
display_height=config.CONFIG_DATA["display"]["DISPLAY_HEIGHT"],
6866
update_queue=config.update_queue)
69-
elif CONFIG_DATA["display"]["REVISION"] == "B":
70-
self.lcd = LcdCommRevB(com_port=CONFIG_DATA['config']['COM_PORT'],
71-
display_width=CONFIG_DATA["display"]["DISPLAY_WIDTH"],
72-
display_height=CONFIG_DATA["display"]["DISPLAY_HEIGHT"],
67+
elif config.CONFIG_DATA["display"]["REVISION"] == "B":
68+
self.lcd = LcdCommRevB(com_port=config.CONFIG_DATA['config']['COM_PORT'],
69+
display_width=config.CONFIG_DATA["display"]["DISPLAY_WIDTH"],
70+
display_height=config.CONFIG_DATA["display"]["DISPLAY_HEIGHT"],
7371
update_queue=config.update_queue)
74-
elif CONFIG_DATA["display"]["REVISION"] == "SIMU":
75-
self.lcd = LcdSimulated(display_width=CONFIG_DATA["display"]["DISPLAY_WIDTH"],
76-
display_height=CONFIG_DATA["display"]["DISPLAY_HEIGHT"])
72+
elif config.CONFIG_DATA["display"]["REVISION"] == "SIMU":
73+
self.lcd = LcdSimulated(display_width=config.CONFIG_DATA["display"]["DISPLAY_WIDTH"],
74+
display_height=config.CONFIG_DATA["display"]["DISPLAY_HEIGHT"])
7775
else:
78-
logger.error("Unknown display revision '", CONFIG_DATA["display"]["REVISION"], "'")
76+
logger.error("Unknown display revision '", config.CONFIG_DATA["display"]["REVISION"], "'")
7977

8078
def initialize_display(self):
8179
# Reset screen in case it was in an unstable state (screen is also cleared)
@@ -88,40 +86,41 @@ def initialize_display(self):
8886
self.lcd.ScreenOn()
8987

9088
# Set brightness
91-
self.lcd.SetBrightness(CONFIG_DATA["display"]["BRIGHTNESS"])
89+
self.lcd.SetBrightness(config.CONFIG_DATA["display"]["BRIGHTNESS"])
9290

9391
# Set backplate RGB LED color (for supported HW only)
94-
self.lcd.SetBackplateLedColor(THEME_DATA['display'].get("DISPLAY_RGB_LED", (255, 255, 255)))
92+
self.lcd.SetBackplateLedColor(config.THEME_DATA['display'].get("DISPLAY_RGB_LED", (255, 255, 255)))
9593

9694
# Set orientation
9795
self.lcd.SetOrientation(_get_theme_orientation())
9896

9997
def display_static_images(self):
100-
if THEME_DATA.get('static_images', False):
101-
for image in THEME_DATA['static_images']:
98+
if config.THEME_DATA.get('static_images', False):
99+
for image in config.THEME_DATA['static_images']:
102100
logger.debug(f"Drawing Image: {image}")
103101
self.lcd.DisplayBitmap(
104-
bitmap_path=THEME_DATA['PATH'] + THEME_DATA['static_images'][image].get("PATH"),
105-
x=THEME_DATA['static_images'][image].get("X", 0),
106-
y=THEME_DATA['static_images'][image].get("Y", 0),
107-
width=THEME_DATA['static_images'][image].get("WIDTH", 0),
108-
height=THEME_DATA['static_images'][image].get("HEIGHT", 0)
102+
bitmap_path=config.THEME_DATA['PATH'] + config.THEME_DATA['static_images'][image].get("PATH"),
103+
x=config.THEME_DATA['static_images'][image].get("X", 0),
104+
y=config.THEME_DATA['static_images'][image].get("Y", 0),
105+
width=config.THEME_DATA['static_images'][image].get("WIDTH", 0),
106+
height=config.THEME_DATA['static_images'][image].get("HEIGHT", 0)
109107
)
110108

111109
def display_static_text(self):
112-
if THEME_DATA.get('static_text', False):
113-
for text in THEME_DATA['static_text']:
110+
if config.THEME_DATA.get('static_text', False):
111+
for text in config.THEME_DATA['static_text']:
114112
logger.debug(f"Drawing Text: {text}")
115113
self.lcd.DisplayText(
116-
text=THEME_DATA['static_text'][text].get("TEXT"),
117-
x=THEME_DATA['static_text'][text].get("X", 0),
118-
y=THEME_DATA['static_text'][text].get("Y", 0),
119-
font=THEME_DATA['static_text'][text].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
120-
font_size=THEME_DATA['static_text'][text].get("FONT_SIZE", 10),
121-
font_color=THEME_DATA['static_text'][text].get("FONT_COLOR", (0, 0, 0)),
122-
background_color=THEME_DATA['static_text'][text].get("BACKGROUND_COLOR", (255, 255, 255)),
123-
background_image=_get_full_path(THEME_DATA['PATH'],
124-
THEME_DATA['static_text'][text].get("BACKGROUND_IMAGE", None))
114+
text=config.THEME_DATA['static_text'][text].get("TEXT"),
115+
x=config.THEME_DATA['static_text'][text].get("X", 0),
116+
y=config.THEME_DATA['static_text'][text].get("Y", 0),
117+
font=config.THEME_DATA['static_text'][text].get("FONT", "roboto-mono/RobotoMono-Regular.ttf"),
118+
font_size=config.THEME_DATA['static_text'][text].get("FONT_SIZE", 10),
119+
font_color=config.THEME_DATA['static_text'][text].get("FONT_COLOR", (0, 0, 0)),
120+
background_color=config.THEME_DATA['static_text'][text].get("BACKGROUND_COLOR", (255, 255, 255)),
121+
background_image=_get_full_path(config.THEME_DATA['PATH'],
122+
config.THEME_DATA['static_text'][text].get("BACKGROUND_IMAGE",
123+
None))
125124
)
126125

127126

library/scheduler.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@
2626

2727
import library.config as config
2828
import library.stats as stats
29-
from library.log import logger
30-
31-
THEME_DATA = config.THEME_DATA
3229

3330
STOPPING = False
3431

@@ -82,68 +79,68 @@ def wrap(
8279

8380

8481
@async_job("CPU_Percentage")
85-
@schedule(timedelta(seconds=THEME_DATA['STATS']['CPU']['PERCENTAGE'].get("INTERVAL", None)).total_seconds())
82+
@schedule(timedelta(seconds=config.THEME_DATA['STATS']['CPU']['PERCENTAGE'].get("INTERVAL", None)).total_seconds())
8683
def CPUPercentage():
8784
""" Refresh the CPU Percentage """
8885
# logger.debug("Refresh CPU Percentage")
8986
stats.CPU.percentage()
9087

9188

9289
@async_job("CPU_Frequency")
93-
@schedule(timedelta(seconds=THEME_DATA['STATS']['CPU']['FREQUENCY'].get("INTERVAL", None)).total_seconds())
90+
@schedule(timedelta(seconds=config.THEME_DATA['STATS']['CPU']['FREQUENCY'].get("INTERVAL", None)).total_seconds())
9491
def CPUFrequency():
9592
""" Refresh the CPU Frequency """
9693
# logger.debug("Refresh CPU Frequency")
9794
stats.CPU.frequency()
9895

9996

10097
@async_job("CPU_Load")
101-
@schedule(timedelta(seconds=THEME_DATA['STATS']['CPU']['LOAD'].get("INTERVAL", None)).total_seconds())
98+
@schedule(timedelta(seconds=config.THEME_DATA['STATS']['CPU']['LOAD'].get("INTERVAL", None)).total_seconds())
10299
def CPULoad():
103100
""" Refresh the CPU Load """
104101
# logger.debug("Refresh CPU Load")
105102
stats.CPU.load()
106103

107104

108105
@async_job("CPU_Load")
109-
@schedule(timedelta(seconds=THEME_DATA['STATS']['CPU']['TEMPERATURE'].get("INTERVAL", None)).total_seconds())
106+
@schedule(timedelta(seconds=config.THEME_DATA['STATS']['CPU']['TEMPERATURE'].get("INTERVAL", None)).total_seconds())
110107
def CPUTemperature():
111108
""" Refresh the CPU Temperature """
112109
# logger.debug("Refresh CPU Temperature")
113110
stats.CPU.temperature()
114111

115112

116113
@async_job("GPU_Stats")
117-
@schedule(timedelta(seconds=THEME_DATA['STATS']['GPU'].get("INTERVAL", None)).total_seconds())
114+
@schedule(timedelta(seconds=config.THEME_DATA['STATS']['GPU'].get("INTERVAL", None)).total_seconds())
118115
def GpuStats():
119116
""" Refresh the GPU Stats """
120117
# logger.debug("Refresh GPU Stats")
121118
stats.Gpu.stats()
122119

123120

124121
@async_job("Memory_Stats")
125-
@schedule(timedelta(seconds=THEME_DATA['STATS']['MEMORY'].get("INTERVAL", None)).total_seconds())
122+
@schedule(timedelta(seconds=config.THEME_DATA['STATS']['MEMORY'].get("INTERVAL", None)).total_seconds())
126123
def MemoryStats():
127124
# logger.debug("Refresh memory stats")
128125
stats.Memory.stats()
129126

130127

131128
@async_job("Disk_Stats")
132-
@schedule(timedelta(seconds=THEME_DATA['STATS']['DISK'].get("INTERVAL", None)).total_seconds())
129+
@schedule(timedelta(seconds=config.THEME_DATA['STATS']['DISK'].get("INTERVAL", None)).total_seconds())
133130
def DiskStats():
134131
# logger.debug("Refresh disk stats")
135132
stats.Disk.stats()
136133

137134

138135
@async_job("Net_Stats")
139-
@schedule(timedelta(seconds=THEME_DATA['STATS']['NET'].get("INTERVAL", None)).total_seconds())
136+
@schedule(timedelta(seconds=config.THEME_DATA['STATS']['NET'].get("INTERVAL", None)).total_seconds())
140137
def NetStats():
141138
# logger.debug("Refresh net stats")
142139
stats.Net.stats()
143140

144141

145142
@async_job("Date_Stats")
146-
@schedule(timedelta(seconds=THEME_DATA['STATS']['DATE'].get("INTERVAL", None)).total_seconds())
143+
@schedule(timedelta(seconds=config.THEME_DATA['STATS']['DATE'].get("INTERVAL", None)).total_seconds())
147144
def DateStats():
148145
# logger.debug("Refresh date stats")
149146
stats.Date.stats()

library/sensors/sensors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def frequency() -> float:
3636

3737
@staticmethod
3838
@abstractmethod
39-
def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg
39+
def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg (%)
4040
pass
4141

4242
@staticmethod

library/sensors/sensors_librehardwaremonitor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def frequency() -> float:
135135
return math.nan
136136

137137
@staticmethod
138-
def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg:
138+
def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg (%):
139139
# Get this data from psutil because it is not available from LibreHardwareMonitor
140140
return psutil.getloadavg()
141141

library/sensors/sensors_python.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def frequency() -> float:
6666
return psutil.cpu_freq().current
6767

6868
@staticmethod
69-
def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg:
69+
def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg (%):
7070
return psutil.getloadavg()
7171

7272
@staticmethod

library/sensors/sensors_stub_random.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def frequency() -> float:
3535
return random.uniform(800, 3400)
3636

3737
@staticmethod
38-
def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg:
38+
def load() -> Tuple[float, float, float]: # 1 / 5 / 15min avg (%):
3939
return random.uniform(0, 100), random.uniform(0, 100), random.uniform(0, 100)
4040

4141
@staticmethod

0 commit comments

Comments
 (0)