flexPTP-test/gui/SetupTab.py
2026-05-07 10:42:36 +02:00

447 lines
17 KiB
Python

from collections.abc import Callable
import tkinter as tk
from tkinter import ttk
from typing import Callable, Literal
import serial.tools.list_ports
import netifaces
import gui.GuiCommon as gcm
class FlexPtpOptionsPanel(ttk.LabelFrame):
def __init_layout(self) -> None:
self.rowconfigure([0, 1, 2, 3 ], weight=1)
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=3)
def __init_widgets(self) -> None:
# initialize labels
device_label = ttk.Label(self, text="Device:") # Serial device entry
device_label.grid(column=0, row=0, sticky=tk.E)
baudrate_label = ttk.Label(self, text="Baudrate:") # Baudrate entry
baudrate_label.grid(column=0, row=1, sticky=tk.E)
baudrate_label = ttk.Label(self, text="Parity:") # Parity selector line
baudrate_label.grid(column=0, row=2, sticky=tk.E)
baudrate_label = ttk.Label(self, text="Stopbits:") # Number of stopbits
baudrate_label.grid(column=0, row=3, sticky=tk.E)
# initialize value selectors
# Serial device
self.__sel_device = tk.StringVar() # device var
device_combobox = ttk.Combobox(self, textvariable=self.__sel_device)
device_combobox["values"] = list(map(lambda p: p.device, filter(lambda p: p.subsystem == "usb", serial.tools.list_ports.comports()))) # list of possible devices
device_combobox.current(0)
device_combobox.grid(column=1, row=0, sticky=tk.EW, padx=4)
# Baudrate (default: 115200)
self.__sel_baudrate = tk.IntVar() # baudrate var
baudrate_combobox = ttk.Combobox(self, textvariable=self.__sel_baudrate)
baudrate_combobox["values"] = (115200, 57600, 38400, 19200, 9600, 4800) # possible baudrates
baudrate_combobox.current(0)
baudrate_combobox.grid(column=1, row=1, sticky=tk.EW, padx=4)
# Parity (default: NONE)
self.__sel_parity = tk.StringVar() # parity var
self.__sel_parity.set(serial.PARITY_NONE)
parity_frame = tk.Frame(self)
parity_frame.grid(column=1, row=2, sticky=tk.W)
parity_none = ttk.Radiobutton(parity_frame, text="None", variable=self.__sel_parity, value=serial.PARITY_NONE) # no parity
parity_none.pack(anchor=tk.W, side=tk.LEFT)
parity_even = ttk.Radiobutton(parity_frame, text="Even", variable=self.__sel_parity, value=serial.PARITY_EVEN) # even parity
parity_even.pack(anchor=tk.W, side=tk.LEFT)
parity_odd = ttk.Radiobutton(parity_frame, text="Odd", variable=self.__sel_parity, value=serial.PARITY_ODD) # odd parity
parity_odd.pack(anchor=tk.W, side=tk.LEFT)
# Stopbits (default: 1)
self.__sel_stopbits = tk.IntVar()
self.__sel_stopbits.set(1)
stopbits_frame = tk.Frame(self)
stopbits_frame.grid(column=1, row=3, sticky=tk.W)
stopbits_one = ttk.Radiobutton(stopbits_frame, text="1", variable=self.__sel_stopbits, value=1)
stopbits_one.pack(anchor=tk.W, side=tk.LEFT)
stopbits_two = ttk.Radiobutton(stopbits_frame, text="2", variable=self.__sel_stopbits, value=2)
stopbits_two.pack(anchor=tk.W, side=tk.LEFT)
def __init__(self, master: tk.Misc | None = None) -> None:
super().__init__(master)
self.configure(labelwidget=ttk.Label(self, text="flexPTP options", font=gcm.TITLE_FONT))
self.__init_layout() # initialize panel's inner layout
self.__init_widgets() # initialize widgets
def get_settings(self) -> dict:
return {
"device": self.__sel_device.get(),
"baudrate": self.__sel_baudrate.get(),
"parity": self.__sel_parity.get(),
"stopbits": self.__sel_stopbits.get()
}
class LinuxPtpOptionsPanel(ttk.LabelFrame):
def __init_layout(self) -> None:
self.rowconfigure([0, 1, 2], weight=1)
# self.rowconfigure(2, weight=100)
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=3)
def __init_widgets(self) -> None:
# initialize labels
path_label = ttk.Label(self, text="Path:") # linuxptp binary path
path_label.grid(column=0, row=0, sticky=tk.E)
if_label = ttk.Label(self, text="Interface:") # network interface
if_label.grid(column=0, row=1, sticky=tk.E)
arg_label = ttk.Label(self, text="Arguments:") # additional arguments passed to the linuxptp
arg_label.grid(column=0, row=2, sticky=tk.E)
# initialize entry widgets
# linuxptp binary path entry (default: /usr/sbin/ptp4l)
self.__linuxptp_path = tk.StringVar() # path var
self.__linuxptp_path.set("/usr/sbin/ptp4l")
path_entry = ttk.Entry(self, font=gcm.TERMINAL_FONT, textvariable=self.__linuxptp_path)
path_entry.grid(column=1, row=0, sticky=tk.EW, padx=4)
# linuxptp interface selector (default: first interface in list)
self.__linuxptp_interface = tk.StringVar() # interface var
baudrate_combobox = ttk.Combobox(self, textvariable=self.__linuxptp_interface)
baudrate_combobox["values"] = netifaces.interfaces() # list of available interfaces
baudrate_combobox.current(0)
baudrate_combobox.grid(column=1, row=1, sticky=tk.EW, padx=4)
# linuxptp additional argument entry (default: <empty>)
self.__linuxptp_args = tk.StringVar() # argument var
self.__linuxptp_args.set("")
args_entry = ttk.Entry(self, font=gcm.TERMINAL_FONT, textvariable=self.__linuxptp_args)
args_entry.grid(column=1, row=2, sticky=tk.EW, padx=4)
def __init__(self, master: tk.Misc | None = None) -> None:
super().__init__(master)
self.configure(labelwidget=ttk.Label(self, text="linuxptp options", font=gcm.TITLE_FONT))
self.__init_layout() # initialize layout
self.__init_widgets() # initialize widgets
def get_settings(self) -> dict:
return {
"path": self.__linuxptp_path.get(),
"interface": self.__linuxptp_interface.get(),
"arguments": self.__linuxptp_args.get()
}
class BasicTestCasesPanel(ttk.LabelFrame):
# test modes and grid positions
MODES = {
"e2e_l4": {
"row": 1,
"col": 1,
},
"e2e_l2": {
"row": 2,
"col": 1,
},
"p2p_l4": {
"row": 1,
"col": 2,
},
"p2p_l2": {
"row": 2,
"col": 2,
},
}
def __init_styles(self) -> None:
style = ttk.Style()
style.configure("BasicTestChkBox.TCheckbutton", font=gcm.TERMINAL_FONT)
style.configure("BasicTestSquare.TFrame", bordercolor="#003039", borderwidth=1, relief="solid")
style.configure("BasicTestLabel.TLabel", font=gcm.MODE_FONT)
def __init_layout(self) -> None:
self.rowconfigure([0], weight=2)
self.rowconfigure([1, 2], weight=8)
self.columnconfigure([0], weight=2)
self.columnconfigure([1, 2], weight=8)
def __init_testcase_grid(self) -> None:
# initialize labels
e2e_label = ttk.Label(self, text="E2E", style="BasicTestLabel.TLabel", padding=6)
e2e_label.grid(row=0, column=1)
p2p_label = ttk.Label(self, text="P2P", style="BasicTestLabel.TLabel")
p2p_label.grid(row=0, column=2)
l4_label = ttk.Label(self, text="L4", style="BasicTestLabel.TLabel", padding=10)
l4_label.grid(row=1, column=0)
l2_label = ttk.Label(self, text="L2", style="BasicTestLabel.TLabel")
l2_label.grid(row=2, column=0)
# initialize mode selectors for all test cases
self.__test_cases = {}
for name, pos in self.MODES.items():
# cell frame containing all checkboxes
oframe = ttk.Frame(self, width=100, height=100, style="BasicTestSquare.TFrame")
oframe.grid(row=pos["row"], column=pos["col"], sticky=tk.NSEW) # place to their designated grid position
oframe.pack_propagate(False)
# internal frame
iframe = tk.Frame(oframe)
iframe.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
# "Master" checkbox
m = tk.BooleanVar()
m_chk = ttk.Checkbutton(iframe, text="Master", variable=m, style="BasicTestChkBox.TCheckbutton")
m_chk.pack(anchor=tk.CENTER, side=tk.TOP)
# "Slave" checkbox
s = tk.BooleanVar()
s_chk = ttk.Checkbutton(iframe, text="Slave ", variable=s, style="BasicTestChkBox.TCheckbutton")
s_chk.pack(anchor=tk.CENTER, side=tk.BOTTOM)
# store test case
self.__test_cases[name] = {
"type": "general",
"delmech": name[0:3].upper(),
"layer": name[4:6].upper(),
"master": m,
"slave": s
}
def __init__(self, master: tk.Misc | None = None) -> None:
super().__init__(master, text="Basic modes")
self.__init_styles() # initialize widget styles
self.__init_layout() # initialize the layout
self.__init_testcase_grid() # initialize the grid widgets
def get_test_cases(self) -> dict:
return self.__test_cases
class DefinedTestCasesPanel(ttk.LabelFrame):
DEFINED_PROFILES = [ "gPTP" ]
PROFILE_FONT = ("Ubuntu", 12, "italic")
def __init_styles(self) -> None:
style = ttk.Style()
style.configure("DefinedTestChkBox.TCheckbutton", font=gcm.TERMINAL_FONT)
def __init_widgets(self) -> None:
self.__test_cases = {}
ri = 0
for dprof in self.DEFINED_PROFILES:
# test case label
label = ttk.Label(self, text=dprof + ":", font=self.PROFILE_FONT)
label.grid(row=ri, column=0, padx=6)
# "Master" checkbox
m = tk.BooleanVar()
m_chk = ttk.Checkbutton(self, text="Master", variable=m, style="DefinedTestChkBox.TCheckbutton")
m_chk.grid(row=ri, column=1)
# "Slave" checkbox
s = tk.BooleanVar()
s_chk = ttk.Checkbutton(self, text="Slave ", variable=s, style="DefinedTestChkBox.TCheckbutton")
s_chk.grid(row=ri, column=2)
# store test case
self.__test_cases[dprof] = {
"type": "defined",
"master": m,
"slave": s
}
def __init__(self, master: tk.Misc | None = None) -> None:
super().__init__(master, text="Defined profiles")
self.__init_styles()
self.__init_widgets()
def get_test_cases(self) -> dict:
return self.__test_cases
class TestCasePanel(ttk.LabelFrame):
def __init_layout(self) -> None:
pass
def __init_widgets(self) -> None:
self.__basic_test_panel = BasicTestCasesPanel(self)
self.__basic_test_panel.pack(expand=False, fill=tk.BOTH, anchor=tk.W, side=tk.LEFT, padx=4, pady=4)
self.__defined_test_panel = DefinedTestCasesPanel(self)
self.__defined_test_panel.pack(expand=True, fill="both", anchor=tk.W, side=tk.LEFT, padx=4, pady=4)
def __init__(self, master: tk.Misc | None = None) -> None:
super().__init__(master)
self.configure(labelwidget= ttk.Label(self, text="Test cases", font=gcm.TITLE_FONT))
self.__init_layout()
self.__init_widgets()
def get_test_cases(self) -> dict:
return dict(self.__basic_test_panel.get_test_cases()) | dict(self.__defined_test_panel.get_test_cases())
class TestManagerMonitorPanel(ttk.LabelFrame):
def __init_layout(self) -> None:
pass
def __init_widgets(self) -> None:
self.__start_stop_btn = ttk.Button(self, text="START", command=self.__start_stop_tests)
self.__start_stop_btn.pack()
tw = ttk.Treeview(self, columns=("name", "result", "start", "end", "duration"), selectmode="none")
tw.heading("#0", text="#")
tw.heading("name", text="Name")
tw.heading("result", text="Result")
tw.heading("start", text="Start")
tw.heading("end", text="End")
tw.heading("duration", text="Duration")
tw.tag_configure("passed", background="LawnGreen")
tw.tag_configure("failed", background="Salmon")
tw.tag_configure("errored", background="firebrick4", foreground="white")
tw.tag_configure("in_progress", background="Gold")
tw.tag_configure("pending", font=("TkDefaultFont", 9, "italic"), foreground="Gray")
tw.pack(expand=True, fill="x")
self.__test_tw = tw
def __init__(self, master: tk.Misc) -> None:
super().__init__(master)
self.configure(labelwidget=ttk.Label(self, text="Test controls", font=gcm.TITLE_FONT))
self.__start_stop_tests_cb: Callable[..., Literal["stopped", "running"]] | None = None
self.__init_layout()
self.__init_widgets()
def __start_stop_tests(self) -> None:
# momentarily disable the start/stop button
self.__start_stop_btn.configure(state="disabled")
# invoke start/stop callback
test_state: Literal["stopped", "running"] = "stopped"
if self.__start_stop_tests_cb is not None:
test_state = self.__start_stop_tests_cb()
# set button state according to the test state (running/stopped)
self.set_test_state(test_state)
# re-enable start/stop button
self.__start_stop_btn.configure(state="enabled")
def register_start_stop_tests_cb(self, start_stop_tests_cb: Callable) -> None:
self.__start_stop_tests_cb = start_stop_tests_cb
def get_test_treeview(self) -> ttk.Treeview:
return self.__test_tw
def set_test_state(self, state: Literal["stopped", "running"]) -> None:
# set button state according to the test state (running/stopped)
if state == "running":
self.__start_stop_btn.configure(text="STOP")
else:
self.__start_stop_btn.configure(text="START")
class SetupTab(ttk.Frame):
def __init_layout(self) -> None:
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=4)
self.rowconfigure([0, 1], weight=1)
self.rowconfigure(2, weight=100)
def __init_widgets(self) -> None:
self.__flexPtp_options_panel = FlexPtpOptionsPanel(self)
self.__flexPtp_options_panel.grid(row=0, column=0, sticky=tk.NSEW, ipady=4, padx=4)
self.__linuxptp_options_panel = LinuxPtpOptionsPanel(self)
self.__linuxptp_options_panel.grid(row=1, rowspan=1, column=0, sticky=tk.NSEW, ipady=4, padx=4)
self.__test_case_panel = TestCasePanel(self)
self.__test_case_panel.grid(row=0, rowspan=2, column=1, sticky=tk.NSEW, ipady=4, padx=4)
self.__test_mgr_monitor_panel = TestManagerMonitorPanel(self)
self.__test_mgr_monitor_panel.grid(row=2, column=0, columnspan=2, sticky=tk.NSEW, ipady=4, padx=4)
def __init__(self, master: tk.Misc | None = None) -> None:
super().__init__(master)
self.__init_layout()
self.__init_widgets()
self.__start_stop_tests_cb : Callable[..., Literal["stopped", "running"]] | None = None
self.__test_mgr_monitor_panel.register_start_stop_tests_cb(self.__start_stop_tests)
def __start_stop_tests(self) -> str:
test_state: Literal["stopped", "running"] = "stopped"
if self.__start_stop_tests_cb is not None: # invoke callback
test_state = self.__start_stop_tests_cb()
# set widget state by test state
self.set_test_state(test_state)
# return with current test state
return test_state
def register_start_stop_tests_cb(self, start_stop_tests_cb) -> None:
self.__start_stop_tests_cb = start_stop_tests_cb
def get_settings(self) -> dict:
settings = {
"flexPTP": self.__flexPtp_options_panel.get_settings(),
"linuxptp": self.__linuxptp_options_panel.get_settings()
}
return settings
def set_option_widgets_state(self, state: Literal["normal", "disabled"]) -> None:
# collect frames to deal with
frames = [ self.__flexPtp_options_panel, self.__linuxptp_options_panel, self.__test_case_panel ]
# create a recursive state configuration function that traverses widgets
def set_widget_state(parent: tk.Widget | tk.Toplevel) -> None:
for widget in parent.winfo_children():
if len(widget.winfo_children()) == 0 and widget.widgetName != "frame":
widget.configure(state=state) # type: ignore
else:
set_widget_state(widget)
# apply required state to all specified frames
for frame in frames:
set_widget_state(frame)
def get_test_cases(self) -> dict:
return self.__test_case_panel.get_test_cases()
def get_test_treeview(self) -> ttk.Treeview:
return self.__test_mgr_monitor_panel.get_test_treeview()
def set_test_state(self, state: Literal["stopped", "running"]) -> None:
self.__test_mgr_monitor_panel.set_test_state(state)
if state == "running":
self.set_option_widgets_state("disabled")
else:
self.set_option_widgets_state("normal")