Skip to content

Commit

Permalink
Migrate the GUI Platform to CutomTkinter (#51)
Browse files Browse the repository at this point in the history
* Migrating to CustomTkinter.

* Migrating to CustomTkinter. (#46)

* Migrating to CustomTkinter. (#46)

* Migrating to CustomTkinter. (#46)

* Migrating to CustomTkinter. (#46)

* Migrating to CustomTkinter. (#46)

* Optimization: used math.lcm() to replace product calculation.

* Bug fixed: grids do not fill the screen.

* Removed config `scaling_factor`.
Changed font sizes and components scaling.
  • Loading branch information
ATATC authored Feb 14, 2024
1 parent 3316c8e commit 4215a9d
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 120 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ The codes are never designed for average high school students to understand. You
Note that LEADS requires **Python >= 3.11**.

```shell
pip install pysimplegui keyboard pyserial leads
pip install customtkinter keyboard pyserial leads
```

`numpy` will be automatically installed with `leads`.

`pysimplegui`, `keyboard`, and `pyserial` are optional.
`customtkinter`, `keyboard`, and `pyserial` are optional.

If you only want the framework, run the following.

Expand Down
9 changes: 4 additions & 5 deletions leads_gui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ def __init__(self, base: dict[str, _Any]) -> None:
self.no_title_bar: bool = False
self.manual_mode: bool = False
self.refresh_rate: int = 30
self.font_size_small: int = 8
self.font_size_medium: int = 16
self.font_size_large: int = 32
self.font_size_x_large: int = 48
self.scaling_factor: float = .8 if get_system_platform() == "linux" else 1
self.font_size_small: int = 12
self.font_size_medium: int = 24
self.font_size_large: int = 40
self.font_size_x_large: int = 56
self.comm_addr: str = "127.0.0.1"
self.comm_port: int = 16900
self.data_dir: str = "./data"
Expand Down
60 changes: 25 additions & 35 deletions leads_gui/prototype.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from threading import Thread as _Thread
from math import lcm as _lcm
from time import sleep as _sleep
from typing import Callable as _Callable, Self as _Self, TypeVar as _TypeVar, Generic as _Generic

from PySimpleGUI import Window as _Window, Element as _Element, WINDOW_CLOSED as _WINDOW_CLOSED, theme as _theme
from customtkinter import CTk as _CTk, CTkBaseClass as _CTkBaseClass, CTkLabel as _CTkLabel

from leads_gui.runtime import RuntimeData
from leads_gui.system import get_system_platform
Expand All @@ -16,7 +16,7 @@ def default_on_kill() -> None:
pass


Widget: type = _Element
Widget: type = _CTkBaseClass

T = _TypeVar("T", bound=RuntimeData)

Expand All @@ -32,24 +32,21 @@ def __init__(self,
title: str = "LEADS",
fullscreen: bool = True,
no_title_bar: bool = True) -> None:
_theme("Default1")
self._root: _Window = _Window(title,
size=(None if width < 0 else width, None if height < 0 else height),
text_justification="center",
no_titlebar=no_title_bar)
self._width: int = _Window.get_screen_size()[0] if fullscreen else width
self._height: int = _Window.get_screen_size()[1] if fullscreen else height
self._fullscreen: bool = fullscreen
self._root: _CTk = _CTk()
self._root.title(title)
self._root.overrideredirect(no_title_bar)
self._width: int = self._root.winfo_screenwidth() if fullscreen else width
self._height: int = self._root.winfo_screenheight() if fullscreen else height
self._root.geometry(str(self._width) + "x" + str(self._height))
self._refresh_rate: int = refresh_rate
self._refresh_interval: float = float(1 / refresh_rate)
self._runtime_data: T = runtime_data
self._on_refresh: _Callable[[_Self], None] = on_refresh
self._on_kill: _Callable[[_Self], None] = on_kill

self._active: bool = False
self._refresher_thread: _Thread | None = None

def root(self) -> _Window:
def root(self) -> _CTk:
return self._root

def width(self) -> int:
Expand All @@ -76,32 +73,17 @@ def set_on_close(self, on_close: _Callable[[_Self], None]) -> None:
def active(self) -> bool:
return self._active

def refresher(self) -> None:
while self._active:
self._root.write_event_value("refresher", None)
self._runtime_data.frame_counter += 1
_sleep(self._refresh_interval)

def show(self) -> None:
self._root.finalize()
if self._fullscreen:
self._root.maximize()
self._active = True
self._refresher_thread = _Thread(name="refresher", target=self.refresher)
self._refresher_thread.start()
while self._active:
event, values = self._root.read()
if event == _WINDOW_CLOSED:
self._active = False
break
elif event == "refresher":
self._on_refresh(self)
elif callable(event):
event()
self._on_refresh(self)
self._root.update()
_sleep(self._refresh_interval)
self._runtime_data.frame_counter += 1

def kill(self) -> None:
self._active = False
self._root.close()
self._root.destroy()


class ContextManager(object):
Expand Down Expand Up @@ -134,7 +116,15 @@ def parse_layout(self, layout: list[list[str | Widget]]) -> list[list[Widget]]:
return layout

def layout(self, layout: list[list[str | Widget]]) -> None:
self._window.root().layout(self.parse_layout(layout))
layout = self.parse_layout(layout)
t = _lcm(*tuple(map(len, layout)))
self.root().grid_columnconfigure(tuple(range(t)), weight=1)
for i in range(len(layout)):
row = layout[i]
length = len(row)
for j in range(length):
s = int(t / length)
row[j].grid(row=i, column=j * s, sticky="NSEW", columnspan=s, ipadx=4, ipady=2, padx=2)

def window(self) -> Window:
return self._window
Expand All @@ -145,7 +135,7 @@ def rd(self) -> T:
def active(self) -> bool:
return self._window.active()

def root(self) -> _Window:
def root(self) -> _CTk:
return self._window.root()

def show(self) -> None:
Expand Down
138 changes: 60 additions & 78 deletions leads_vec/cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime
from time import time

from PySimpleGUI import Button, Text, Column
from customtkinter import CTkButton, CTkLabel
from keyboard import add_hotkey

from leads import *
Expand All @@ -19,6 +19,12 @@ class CustomRuntimeData(RuntimeData):
def main() -> int:
cfg = get_config(Config)
context = Leads(srw_mode=cfg.srw_mode)
window = Window(cfg.width,
cfg.height,
cfg.refresh_rate,
CustomRuntimeData(),
fullscreen=cfg.fullscreen,
no_title_bar=cfg.no_title_bar)

def render(manager: ContextManager):
def switch_m1_mode():
Expand All @@ -27,73 +33,57 @@ def switch_m1_mode():
def switch_m3_mode():
manager.rd().m3_mode = (manager.rd().m3_mode + 1) % 3

manager["m1"] = Button(font=("Arial", cfg.font_size_small), key=switch_m1_mode,
size=(round(manager.window().width() * cfg.scaling_factor / 21), 13))
manager["m2"] = Button(font=("Arial", cfg.font_size_x_large),
size=(round(manager.window().width() * cfg.scaling_factor / 126), 2))
manager["m3"] = Button(font=("Arial", cfg.font_size_medium), key=switch_m3_mode,
size=(round(manager.window().width() * cfg.scaling_factor / 42), 7))
manager["dtcs_status"] = Text(text="DTCS READY", text_color="green", font=("Arial", cfg.font_size_small),
size=(round(manager.window().width() * cfg.scaling_factor / 40), None))
manager["abs_status"] = Text(text="ABS READY", text_color="green", font=("Arial", cfg.font_size_small),
size=(round(manager.window().width() * cfg.scaling_factor / 40), None))
manager["ebi_status"] = Text(text="EBI READY", text_color="green", font=("Arial", cfg.font_size_small),
size=(round(manager.window().width() * cfg.scaling_factor / 40), None))
manager["atbs_status"] = Text(text="ATBS READY", text_color="green", font=("Arial", cfg.font_size_small),
size=(round(manager.window().width() * cfg.scaling_factor / 40), None))
manager["comm_status"] = Text(text="COMM OFFLINE", text_color="gray", font=("Arial", cfg.font_size_small),
size=(round(manager.window().width() * cfg.scaling_factor / 40), None))
manager["m1"] = CTkButton(window.root(), font=("Arial", cfg.font_size_small), command=switch_m1_mode)
manager["m2"] = CTkButton(window.root(), font=("Arial", cfg.font_size_x_large))
manager["m3"] = CTkButton(window.root(), font=("Arial", cfg.font_size_medium), command=switch_m3_mode)
manager["dtcs_status"] = CTkLabel(window.root(), text="DTCS READY", text_color="green",
font=("Arial", cfg.font_size_small))
manager["abs_status"] = CTkLabel(window.root(), text="ABS READY", text_color="green",
font=("Arial", cfg.font_size_small))
manager["ebi_status"] = CTkLabel(window.root(), text="EBI READY", text_color="green",
font=("Arial", cfg.font_size_small))
manager["atbs_status"] = CTkLabel(window.root(), text="ATBS READY", text_color="green",
font=("Arial", cfg.font_size_small))
manager["comm_status"] = CTkLabel(window.root(), text="COMM OFFLINE", text_color="gray",
font=("Arial", cfg.font_size_small))

def switch_dtcs():
context.set_dtcs(not (dtcs_enabled := context.is_dtcs_enabled()))
manager["dtcs"].update(f"DTCS {'OFF' if dtcs_enabled else 'ON'}")
context.set_dtcs(not context.is_dtcs_enabled())

add_hotkey("1", switch_dtcs)

def switch_abs():
context.set_abs(not (abs_enabled := context.is_abs_enabled()))
manager["abs"].update(f"ABS {'OFF' if abs_enabled else 'ON'}")
context.set_abs(not context.is_abs_enabled())

add_hotkey("2", switch_abs)

def switch_ebi():
context.set_ebi(not (ebi_enabled := context.is_ebi_enabled()))
manager["ebi"].update(f"EBI {'OFF' if ebi_enabled else 'ON'}")
context.set_ebi(not context.is_ebi_enabled())

add_hotkey("3", switch_ebi)

def switch_atbs():
context.set_atbs(not (atbs_enabled := context.is_atbs_enabled()))
manager["atbs"].update(f"ATBS {'OFF' if atbs_enabled else 'ON'}")
context.set_atbs(not context.is_atbs_enabled())

add_hotkey("4", switch_atbs)

manager["dtcs"] = Button(button_text="DTCS ON", key=switch_dtcs, font=("Arial", cfg.font_size_small),
size=(round(manager.window().width() * cfg.scaling_factor / 35), 1))
manager["abs"] = Button(button_text="ABS ON", key=switch_abs, font=("Arial", cfg.font_size_small),
size=(round(manager.window().width() * cfg.scaling_factor / 35), 1))
manager["ebi"] = Button(button_text="EBI ON", key=switch_ebi, font=("Arial", cfg.font_size_small),
size=(round(manager.window().width() * cfg.scaling_factor / 35), 1))
manager["atbs"] = Button(button_text="ATBS ON", key=switch_atbs, font=("Arial", cfg.font_size_small),
size=(round(manager.window().width() * cfg.scaling_factor / 35), 1))

uim = initialize(
Window(cfg.width,
cfg.height,
cfg.refresh_rate,
CustomRuntimeData(),
fullscreen=cfg.fullscreen,
no_title_bar=cfg.no_title_bar),
render,
context,
get_controller(MAIN_CONTROLLER))
manager["dtcs"] = CTkButton(window.root(), text="DTCS ON", command=switch_dtcs,
font=("Arial", cfg.font_size_small))
manager["abs"] = CTkButton(window.root(), text="ABS ON", command=switch_abs,
font=("Arial", cfg.font_size_small))
manager["ebi"] = CTkButton(window.root(), text="EBI ON", command=switch_ebi,
font=("Arial", cfg.font_size_small))
manager["atbs"] = CTkButton(window.root(), text="ATBS ON", command=switch_atbs,
font=("Arial", cfg.font_size_small))

uim = initialize(window, render, context, get_controller(MAIN_CONTROLLER))

class CommCallback(Callback):
def on_fail(self, service: Service, error: Exception) -> None:
L.error("Comm server error: " + str(error))

def on_connect(self, service: Service, connection: Connection) -> None:
uim["comm_status"].update("COMM ONLINE", text_color="black")
uim["comm_status"].configure(text="COMM ONLINE", text_color="black")

uim.rd().comm = start_server(create_server(cfg.comm_port, CommCallback()), True)

Expand All @@ -104,57 +94,49 @@ def on_push(self, e: DataPushedEvent) -> None:
def on_update(self, e: UpdateEvent) -> None:
duration = int(time()) - uim.rd().start_time
if uim.rd().m1_mode == 0:
uim["m1"].update("LAP TIME\n\nLAP1 9s\nLAP2 11s\nLAP3 10s")
uim["m1"].configure(text="LAP TIME\n\nLAP1 9s\nLAP2 11s\nLAP3 10s")
elif uim.rd().m1_mode == 1:
uim["m1"].update(f"VeC {__version__.upper()}\n\n"
f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
f"{duration // 60} MIN {duration % 60} SEC\n\n"
f"{'SRW MODE' if cfg.srw_mode else 'DRW MODE'}\n"
f"REFRESH RATE: {cfg.refresh_rate} FPS")
uim["m1"].configure(text=f"VeC {__version__.upper()}\n\n"
f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
f"{duration // 60} MIN {duration % 60} SEC\n\n"
f"{'SRW MODE' if cfg.srw_mode else 'DRW MODE'}\n"
f"REFRESH RATE: {cfg.refresh_rate} FPS")
if uim.rd().frame_counter % int(cfg.refresh_rate / 4) == 0:
uim["m2"].update(f"{int(context.data().front_wheel_speed)}")
uim["m2"].configure(text=f"{int(context.data().front_wheel_speed)}")
if uim.rd().m3_mode == 0:
uim["m3"].update("0.0V")
uim["m3"].configure(text="0.0V")
elif uim.rd().m3_mode == 1:
uim["m3"].update("G Force")
uim["m3"].configure(text="G Force")
else:
uim["m3"].update("Speed Trend")
uim["m3"].configure(text="Speed Trend")
if uim.rd().comm.num_connections() < 1:
uim["comm_status"].update("COMM OFFLINE", text_color="gray")
uim["comm_status"].configure(text="COMM OFFLINE", text_color="gray")
uim["dtcs"].configure(text=f"DTCS {'OFF' if context.is_dtcs_enabled() else 'ON'}")
uim["abs"].configure(text=f"ABS {'OFF' if context.is_abs_enabled() else 'ON'}")
uim["ebi"].configure(text=f"EBI {'OFF' if context.is_ebi_enabled() else 'ON'}")
uim["atbs"].configure(text=f"ATBS {'OFF' if context.is_atbs_enabled() else 'ON'}")

def on_intervene(self, e: InterventionEvent) -> None:
uim[e.system.lower() + "_status"].update(e.system + " INTEV", text_color="purple")
uim[e.system.lower() + "_status"].configure(text=e.system + " INTEV", text_color="purple")

def post_intervene(self, e: InterventionEvent) -> None:
uim[e.system.lower() + "_status"].update(e.system + " READY", text_color="green")
uim[e.system.lower() + "_status"].configure(text=e.system + " READY", text_color="green")

def on_suspend(self, e: SuspensionEvent) -> None:
uim[e.system.lower() + "_status"].update(e.system + " SUSPD", text_color="red")
uim[e.system.lower() + "_status"].configure(text=e.system + " SUSPD", text_color="red")

def post_suspend(self, e: SuspensionEvent) -> None:
uim[e.system.lower() + "_status"].update(e.system + " READY", text_color="green")
uim[e.system.lower() + "_status"].configure(text=e.system + " READY", text_color="green")

context.set_event_listener(CustomListener())
uim.layout([
[
Column([[uim["m1"]]], element_justification='c', expand_x=True),
Column([[uim["m2"]]], element_justification='c', expand_x=True),
Column([[uim["m3"]]], element_justification='c', expand_x=True)
],
[
Column([[uim["dtcs_status"]]], element_justification='c', expand_x=True),
Column([[uim["abs_status"]]], element_justification='c', expand_x=True),
Column([[uim["ebi_status"]]], element_justification='c', expand_x=True),
Column([[uim["atbs_status"]]], element_justification='c', expand_x=True),
Column([[uim["comm_status"]]], element_justification='c', expand_x=True)
],
[
Column([[uim["dtcs"]]], element_justification='c', expand_x=True),
Column([[uim["abs"]]], element_justification='c', expand_x=True),
Column([[uim["ebi"]]], element_justification='c', expand_x=True),
Column([[uim["atbs"]]], element_justification='c', expand_x=True)
]
["m1", "m2", "m3"],
["dtcs_status", "abs_status", "ebi_status", "atbs_status", "comm_status"],
["dtcs", "abs", "ebi", "atbs"]
])
CTkLabel(uim.root(), text="").grid(row=3, column=0)
uim.root().grid_rowconfigure(0, weight=1)
uim.root().grid_rowconfigure(3, weight=2)
uim.show()
uim.rd().comm_kill()
return 0

0 comments on commit 4215a9d

Please sign in to comment.