Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
633f930
[Configure] Inherit Toplevel and Tk directly
ColdWindScholar Dec 1, 2025
4128233
[theme-editor] rewrite the window to a class
ColdWindScholar Dec 1, 2025
5ce30d4
[turning-theme-extractor] get it more specific
ColdWindScholar Dec 1, 2025
9217153
[theme-editor] use ttk Button instead of TkButton
ColdWindScholar Dec 2, 2025
5bf2e4a
[Configure] Remove unused argument e
ColdWindScholar Dec 2, 2025
0b07b9c
[Configure] Remove duplicate function defines
ColdWindScholar Dec 2, 2025
388c3aa
[display] use dict instead of judge
ColdWindScholar Dec 2, 2025
ccd726c
[config] use isinstance instead of type
ColdWindScholar Dec 2, 2025
b1402f7
[lcd_simulated] use f-string instead of +
ColdWindScholar Dec 2, 2025
005c64e
[lcd_comm_rev_c] use dict instead of judge
ColdWindScholar Dec 2, 2025
c4bd5f2
[test_librehardwaremonitor] add os detect
ColdWindScholar Dec 2, 2025
3df591f
[config] feat: add app theme switcher
ColdWindScholar Dec 2, 2025
6f0bda5
[MAIN] rewrite it to classes
ColdWindScholar Dec 2, 2025
b066a31
[config] THEME_DATA is a dict
ColdWindScholar Dec 2, 2025
f631eeb
[lcd] clean code
ColdWindScholar Dec 4, 2025
7d863e6
[theme-preview-generator] clean code
ColdWindScholar Dec 4, 2025
e2ab00b
[turing-theme-extrcator] clean code
ColdWindScholar Dec 4, 2025
a823911
[theme-editor] make sure RESIZE_FACTOR > 0
ColdWindScholar Dec 4, 2025
6f9938e
[theme-editor] allow change zoom level while running and refresh auto
ColdWindScholar Dec 4, 2025
66aaa8f
[theme-editor] change zoom+- buttons to scale
ColdWindScholar Dec 4, 2025
7b46ce8
[theme-editor] change zoom range
ColdWindScholar Dec 5, 2025
d5fc2d7
[config] rewrite it to a class
ColdWindScholar Dec 5, 2025
f13597c
[config] catch exception to log
ColdWindScholar Dec 5, 2025
ee397fe
[scheduler] Remove duplicate function defines
ColdWindScholar Dec 5, 2025
63a013c
[theme-editor] allow to be imported in other modules
ColdWindScholar Dec 5, 2025
f04e897
[configure] import theme_editor.py insteadof popen
ColdWindScholar Dec 5, 2025
40d5c31
[configure] rewrite loop processing to a def
ColdWindScholar Dec 6, 2025
3104020
[configure] 2rewrite loop processing to a def
ColdWindScholar Dec 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 129 additions & 136 deletions configure.py

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion external/LibreHardwareMonitor/test_librehardwaremonitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import os
import sys
from pathlib import Path

if os.name != 'nt':
raise Exception("This script is only for Windows")
import clr # Clr is from pythonnet package. Do not install clr package
from win32api import *

Expand Down
111 changes: 54 additions & 57 deletions library/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# Copyright (C) 2021 Matthieu Houdebine (mathoudebine)
# Copyright (C) 2022 Rollbacke
# Copyright (C) 2022 Ebag333
# Copyright (C) 2025 ColdWindScholar
#
# 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
Expand All @@ -21,67 +22,63 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import os
import queue
from queue import Queue
import sys
from pathlib import Path
import yaml

from yaml import safe_load
from library.log import logger


def load_yaml(configfile):
with open(configfile, "rt", encoding='utf8') as stream:
yamlconfig = yaml.safe_load(stream)
return yamlconfig


PATH = sys.path[0]
MAIN_DIRECTORY = Path(__file__).parent.parent.resolve()
FONTS_DIR = str(MAIN_DIRECTORY / "res" / "fonts") + "/"
CONFIG_DATA = load_yaml(MAIN_DIRECTORY / "config.yaml")
THEME_DEFAULT = load_yaml(MAIN_DIRECTORY / "res/themes/default.yaml")
THEME_DATA = None


def copy_default(default, theme):
"""recursively supply default values into a dict of dicts of dicts ...."""
for k, v in default.items():
if k not in theme:
theme[k] = v
if type(v) == type({}):
copy_default(default[k], theme[k])


def load_theme():
global THEME_DATA
try:
theme_path = Path("res/themes/" + CONFIG_DATA['config']['THEME'])
logger.info("Loading theme %s from %s" % (CONFIG_DATA['config']['THEME'], theme_path / "theme.yaml"))
THEME_DATA = load_yaml(MAIN_DIRECTORY / theme_path / "theme.yaml")
THEME_DATA['PATH'] = str(MAIN_DIRECTORY / theme_path) + "/"
except:
logger.error("Theme not found or contains errors!")
class Config:
def __init__(self):
self.MAIN_DIRECTORY = Path(__file__).parent.parent.resolve()
self.FONTS_DIR = str(self.MAIN_DIRECTORY / "res" / "fonts") + "/"
self.CONFIG_DATA = self.load_yaml(self.MAIN_DIRECTORY / "config.yaml")
self.THEME_DEFAULT = self.load_yaml(self.MAIN_DIRECTORY / "res/themes/default.yaml")
self.THEME_DATA: dict = {}
# Load theme on import
self.load_theme()
# Queue containing the serial requests to send to the screen
self.update_queue = Queue()

@staticmethod
def load_yaml(configfile: str | Path):
with open(configfile, "rt", encoding='utf8') as stream:
return safe_load(stream)

def copy_default(self, default: dict, theme: dict):
"""recursively supply default values into a dict of dicts of dicts ...."""
for k, v in default.items():
if k not in theme:
theme[k] = v
if isinstance(v, dict):
self.copy_default(default[k], theme[k])

def load_theme(self):
try:
sys.exit(0)
theme_path = Path(f"res/themes/{self.CONFIG_DATA['config']['THEME']}")
logger.info(f"Loading theme {self.CONFIG_DATA['config']['THEME']} from {theme_path / 'theme.yaml'}")
self.THEME_DATA = self.load_yaml(self.MAIN_DIRECTORY / theme_path / "theme.yaml")
self.THEME_DATA['PATH'] = str(self.MAIN_DIRECTORY / theme_path) + "/"
except:
os._exit(0)

copy_default(THEME_DEFAULT, THEME_DATA)


def check_theme_compatible(display_size: str):
# Check if theme is compatible with hardware revision
if display_size != THEME_DATA['display'].get("DISPLAY_SIZE", '3.5"'):
logger.error("The selected theme " + CONFIG_DATA['config'][
'THEME'] + " is not compatible with your display revision " + CONFIG_DATA["display"]["REVISION"])
try:
sys.exit(0)
except:
os._exit(0)


# Load theme on import
load_theme()

# Queue containing the serial requests to send to the screen
update_queue = queue.Queue()
logger.error("Theme not found or contains errors!")
logger.exception('load_theme')
try:
sys.exit(0)
except:
os._exit(0)

self.copy_default(self.THEME_DEFAULT, self.THEME_DATA)

def check_theme_compatible(self, display_size: str):
# Check if theme is compatible with hardware revision
if display_size != self.THEME_DATA['display'].get("DISPLAY_SIZE", '3.5"'):
logger.error(
f"The selected theme {self.CONFIG_DATA['config']['THEME']} is not compatible with your display revision {self.CONFIG_DATA["display"]["REVISION"]}")
try:
sys.exit(0)
except:
os._exit(0)


config = Config()
30 changes: 13 additions & 17 deletions library/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from library import config
from library.config import config
from library.lcd.lcd_comm import Orientation
from library.lcd.lcd_comm_rev_a import LcdCommRevA
from library.lcd.lcd_comm_rev_b import LcdCommRevB
Expand All @@ -33,8 +33,7 @@
def _get_full_path(path, name):
if name:
return path + name
else:
return None
return None


def _get_theme_orientation() -> Orientation:
Expand All @@ -55,20 +54,17 @@ 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"':
return 480, 480
elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '3.5"':
return 320, 480
elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '5"':
return 480, 800
elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '8.8"':
return 480, 1920
else:
sizes = {
'0.96"': (80, 160),
'2.1"': (480, 480),
'3.5"': (320, 480),
'5"': (480, 800),
'8.8"': (480, 1920),
}
if config.THEME_DATA["display"].get("DISPLAY_SIZE", '') not in sizes.keys():
logger.warning(
f'Cannot find valid DISPLAY_SIZE property in selected theme {config.CONFIG_DATA["config"]["THEME"]}, defaulting to 3.5"')
return 320, 480
return sizes.get(config.THEME_DATA["display"].get("DISPLAY_SIZE", ''), (320, 480))


class Display:
Expand All @@ -90,10 +86,10 @@ def __init__(self):
update_queue=config.update_queue)
elif config.CONFIG_DATA["display"]["REVISION"] == "WEACT_A":
self.lcd = LcdCommWeActA(com_port=config.CONFIG_DATA['config']['COM_PORT'],
update_queue=config.update_queue)
update_queue=config.update_queue)
elif config.CONFIG_DATA["display"]["REVISION"] == "WEACT_B":
self.lcd = LcdCommWeActB(com_port=config.CONFIG_DATA['config']['COM_PORT'],
update_queue=config.update_queue)
update_queue=config.update_queue)
elif config.CONFIG_DATA["display"]["REVISION"] == "SIMU":
# Simulated display: always set width/height from theme
self.lcd = LcdSimulated(display_width=width, display_height=height)
Expand Down
11 changes: 6 additions & 5 deletions library/lcd/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
# - "hsl(0, 100%, 50%)"
Color = Union[str, RGBColor]


def parse_color(color: Color) -> RGBColor:
# even if undocumented, let's be nice and accept a list in lieu of a tuple
if isinstance(color, tuple) or isinstance(color, list):
if len(color) != 3:
raise ValueError("RGB color must have 3 values")
return (int(color[0]), int(color[1]), int(color[2]))
return int(color[0]), int(color[1]), int(color[2])

if not isinstance(color, str):
raise ValueError("Color must be either an RGB tuple or a string")
Expand All @@ -42,7 +43,7 @@ def parse_color(color: Color) -> RGBColor:

# fallback as a PIL color
rgbcolor = ImageColor.getrgb(color)
if len(rgbcolor) == 4:
return (rgbcolor[0], rgbcolor[1], rgbcolor[2])
return rgbcolor

if len(rgbcolor) >= 4:
return rgbcolor[0], rgbcolor[1], rgbcolor[2]
else:
return rgbcolor
2 changes: 0 additions & 2 deletions library/lcd/lcd_comm_rev_a.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import time
from enum import Enum
from typing import Optional

from serial.tools.list_ports import comports

Expand Down
4 changes: 2 additions & 2 deletions library/lcd/lcd_comm_rev_b.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def auto_detect_com_port() -> Optional[str]:

return None

def SendCommand(self, cmd: Command, payload=None, bypass_queue: bool = False):
def SendCommand(self, cmd: Command, payload: list = None, bypass_queue: bool = False):
# New protocol (10 byte packets, framed with the command, 8 data bytes inside)
if payload is None:
payload = [0] * 8
Expand Down Expand Up @@ -253,4 +253,4 @@ def DisplayPILImage(
if self.update_queue:
self.update_queue.put((time.sleep, [0.05]))
else:
time.sleep(0.05)
time.sleep(0.05)
16 changes: 8 additions & 8 deletions library/lcd/lcd_comm_rev_c.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ def _hello(self):
response = ''.join(
filter(lambda x: x in set(string.printable), str(self.serial_read(23).decode(errors="ignore"))))
self.serial_flush_input()
logger.debug("Display ID returned: %s" % response)
logger.debug(f"Display ID returned: {response}")
while not response.startswith("chs_"):
logger.warning("Display returned invalid or unsupported ID, try again in 1 second")
time.sleep(1)
Expand All @@ -232,13 +232,13 @@ def _hello(self):

# Note: ID returned by display are not reliable for some models e.g. 2.1" displays return "chs_5inch"
# Rely on width/height for sub-revision detection
if self.display_width == 480 and self.display_height == 480:
self.sub_revision = SubRevision.REV_2INCH
elif self.display_width == 480 and self.display_height == 800:
self.sub_revision = SubRevision.REV_5INCH
elif self.display_width == 480 and self.display_height == 1920:
self.sub_revision = SubRevision.REV_8INCH
else:
sub_revisions = {
(480, 480): SubRevision.REV_2INCH,
(480, 800): SubRevision.REV_5INCH,
(480, 1920): SubRevision.REV_8INCH
}
self.sub_revision = sub_revisions.get((self.display_width, self.display_height), SubRevision.UNKNOWN)
if self.sub_revision == SubRevision.UNKNOWN:
logger.error(f"Unsupported resolution {self.display_width}x{self.display_height} for revision C")

# Detect ROM version
Expand Down
2 changes: 1 addition & 1 deletion library/lcd/lcd_comm_rev_d.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def SetOrientation(self, orientation: Orientation = Orientation.PORTRAIT):
# Basic orientations (portrait / landscape) are software-managed because screen commands only support portrait
self.orientation = orientation

if self.orientation == Orientation.REVERSE_LANDSCAPE or self.orientation == Orientation.REVERSE_PORTRAIT:
if self.orientation in [Orientation.REVERSE_LANDSCAPE, Orientation.REVERSE_PORTRAIT]:
self.SendCommand(cmd=Command.SET180)
else:
self.SendCommand(cmd=Command.SETORG)
Expand Down
11 changes: 6 additions & 5 deletions library/lcd/lcd_comm_weact_a.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def auto_detect_com_port():
for com_port in com_ports:
if com_port.vid == 0x1a86 and com_port.pid == 0xfe0c:
return com_port.device
if type(com_port.serial_number) == str:
if isinstance(com_port.serial_number, str):
if com_port.serial_number.startswith("AB"):
return com_port.device

Expand Down Expand Up @@ -178,7 +178,7 @@ def SetOrientation(self, orientation: Orientation = Orientation.PORTRAIT):
byteBuffer[2] = Command.CMD_END
self.SendCommand(byteBuffer)

def SetSensorReportTime(self, time_ms: int):
def SetSensorReportTime(self, time_ms: int) -> bool:
if time_ms > 0xFFFF or (time_ms < 500 and time_ms != 0):
return False
byteBuffer = bytearray(4)
Expand All @@ -187,6 +187,7 @@ def SetSensorReportTime(self, time_ms: int):
byteBuffer[2] = time_ms >> 8 & 0xFF
byteBuffer[3] = Command.CMD_END
self.SendCommand(byteBuffer)
return True

def Free(self):
byteBuffer = bytearray(2)
Expand All @@ -198,11 +199,11 @@ def HandleSensorReport(self):
if self.lcd_serial.in_waiting > 0:
cmd = self.ReadData(1)
if (
cmd != None
and cmd[0] == Command.CMD_ENABLE_HUMITURE_REPORT | Command.CMD_READ
cmd is not None
and cmd[0] == Command.CMD_ENABLE_HUMITURE_REPORT | Command.CMD_READ
):
data = self.ReadData(5)
if data != None and len(data) == 5 and data[4] == Command.CMD_END:
if data is not None and len(data) == 5 and data[4] == Command.CMD_END:
unpack = struct.unpack("<Hh", data[0:4])
self.temperature = float(unpack[0]) / 100
self.humidness = float(unpack[1]) / 100
Expand Down
2 changes: 1 addition & 1 deletion library/lcd/lcd_comm_weact_b.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def auto_detect_com_port():
for com_port in com_ports:
if com_port.vid == 0x1a86 and com_port.pid == 0xfe0c:
return com_port.device
if type(com_port.serial_number) == str:
if isinstance(com_port.serial_number, str):
if com_port.serial_number.startswith("AD"):
return com_port.device

Expand Down
22 changes: 11 additions & 11 deletions library/lcd/lcd_simulated.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,21 @@ def do_GET(self):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(bytes("<img src=\"" + SCREENSHOT_FILE + "\" id=\"myImage\" />", "utf-8"))
self.wfile.write(bytes(f"<img src=\"{SCREENSHOT_FILE}\" id=\"myImage\" />", "utf-8"))
self.wfile.write(bytes("<script>", "utf-8"))
self.wfile.write(bytes("setInterval(function() {", "utf-8"))
self.wfile.write(bytes(" var myImageElement = document.getElementById('myImage');", "utf-8"))
self.wfile.write(bytes(" myImageElement.src = '" + SCREENSHOT_FILE + "?rand=' + Math.random();", "utf-8"))
self.wfile.write(bytes(f" myImageElement.src = '{SCREENSHOT_FILE}?rand=' + Math.random();", "utf-8"))
self.wfile.write(bytes("}, 250);", "utf-8"))
self.wfile.write(bytes("</script>", "utf-8"))
elif self.path.startswith("/" + SCREENSHOT_FILE):
imgfile = open(SCREENSHOT_FILE, 'rb').read()
mimetype = mimetypes.MimeTypes().guess_type(SCREENSHOT_FILE)[0]
self.send_response(200)
if mimetype is not None:
self.send_header('Content-type', mimetype)
self.end_headers()
self.wfile.write(imgfile)
elif self.path.startswith(f"/{SCREENSHOT_FILE}"):
with open(SCREENSHOT_FILE, 'rb') as imgfile:
mimetype = mimetypes.MimeTypes().guess_type(SCREENSHOT_FILE)[0]
self.send_response(200)
if mimetype is not None:
self.send_header('Content-type', mimetype)
self.end_headers()
self.wfile.write(imgfile.read())


# Simulated display: write on a file instead of serial port
Expand All @@ -67,7 +67,7 @@ def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_hei

try:
self.webServer = HTTPServer(("localhost", WEBSERVER_PORT), SimulatedLcdWebServer)
logger.debug("To see your simulated screen, open http://%s:%d in a browser" % ("localhost", WEBSERVER_PORT))
logger.debug(f"To see your simulated screen, open http://localhost:{WEBSERVER_PORT} in a browser")
threading.Thread(target=self.webServer.serve_forever).start()
except OSError:
logger.error("Error starting webserver! An instance might already be running on port %d." % WEBSERVER_PORT)
Expand Down
Loading