Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate the GUI Platform to CutomTkinter #51

Merged
merged 11 commits into from
Feb 14, 2024
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