flexPTP-test/gui/SetupTab.py
András Wiesner 23e5005914 - Parameter configuration added
- Plotting added
- Statistic calculation added
- Save measurement data as XLSX feature added
- Test controller functionality extracted from the GUI class
2026-05-27 20:06:53 +02:00

610 lines
24 KiB
Python

from collections.abc import Callable
import tkinter as tk
from tkinter import IntVar, StringVar, 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(),
"logSyncInterval": 0,
"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": {
"logSyncInterval": -3
}
}
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_key in self.DEFINED_PROFILES.keys():
# fetch profile
dprof = self.DEFINED_PROFILES[dprof_key]
# test case label
label = ttk.Label(self, text=dprof_key + ":", 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_key] = {
"type": "defined",
"logSyncInterval": dprof["logSyncInterval"],
"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 TestParametersPanel(ttk.LabelFrame):
def __init_styles(self) -> None:
style = ttk.Style()
style.configure("Parameters.TLabel", font=gcm.TERMINAL_FONT)
def __init_layout(self) -> None:
self.columnconfigure([0, 1], weight=1)
def __init_widgets(self) -> None:
# linuxptp initialization timeout
linuxptp_init_timeout_label = ttk.Label(self, text="linuxptp init timeout")
linuxptp_init_timeout_label.grid(row=0, column=0)
# BMCA settle timeout
bmca_timeout_label = ttk.Label(self, text="BMCA timeout")
bmca_timeout_label.grid(row=1, column=0)
# sync timeout
sync_timeout_label = ttk.Label(self, text="Sync timeout")
sync_timeout_label.grid(row=2, column=0)
# minimum number of sync cycles
min_sync_cycles_label = ttk.Label(self, text="Min. sync cycles")
min_sync_cycles_label.grid(row=3, column=0)
# max number of sync cycles
max_sync_cycles_label = ttk.Label(self, text="Max. sync cycles")
max_sync_cycles_label.grid(row=4, column=0)
# target variance
target_variance_label = ttk.Label(self, text="Target variance")
target_variance_label.grid(row=5, column=0)
# averaging window size
averaging_window_size_label = ttk.Label(self, text="Averaging window size")
averaging_window_size_label.grid(row=6, column=0)
# ----------
# linuxptp init timeout
self.__linuxptp_init_timeout_str = StringVar()
linuxptp_init_timeout_spinner = ttk.Spinbox(self, format="%.0f s", textvariable=self.__linuxptp_init_timeout_str)
linuxptp_init_timeout_spinner["from"] = 2
linuxptp_init_timeout_spinner["to"] = 60
linuxptp_init_timeout_spinner.set("5 s")
linuxptp_init_timeout_spinner.grid(row=0, column=1)
# BMCA timeout
self.__bmca_timeout_str = StringVar()
bmca_timeout_spinner = ttk.Spinbox(self, format="%.0f s", textvariable=self.__bmca_timeout_str)
bmca_timeout_spinner["from"] = 2
bmca_timeout_spinner["to"] = 300
bmca_timeout_spinner.set("60 s")
bmca_timeout_spinner.grid(row=1, column=1)
# Sync timeout
self.__sync_timeout_str = StringVar()
sync_timeout_spinner = ttk.Spinbox(self, format="%.0f cycles", textvariable=self.__sync_timeout_str)
sync_timeout_spinner["from"] = 2
sync_timeout_spinner["to"] = 300
sync_timeout_spinner.set("4 cycles")
sync_timeout_spinner.grid(row=2, column=1)
def crosscheck_min_max_cycles(correct_this: Literal["min", "max"]) -> None:
min_cycles = int(self.__min_sync_cycles_str.get()[:-6])
max_cycles = int(self.__max_sync_cycles_str.get()[:-6])
match correct_this:
case "min":
if max_cycles <= min_cycles:
min_cycles = max_cycles - 1
case "max":
if min_cycles >= max_cycles:
max_cycles = min_cycles + 1
self.__min_sync_cycles_str.set("{:d} cycles".format(min_cycles))
self.__max_sync_cycles_str.set("{:d} cycles".format(max_cycles))
def crosscheck_based_on_min() -> None:
crosscheck_min_max_cycles("max")
def crosscheck_based_on_max() -> None:
crosscheck_min_max_cycles("min")
# Min Sync cycles
self.__min_sync_cycles_str = StringVar()
max_sync_cycles_spinner = ttk.Spinbox(self, format="%.0f cycles", textvariable=self.__min_sync_cycles_str, command=crosscheck_based_on_min)
max_sync_cycles_spinner["from"] = 0
max_sync_cycles_spinner["to"] = 10000000 - 1
max_sync_cycles_spinner.set("0 cycles")
max_sync_cycles_spinner.grid(row=3, column=1)
# Max Sync cycles
self.__max_sync_cycles_str = StringVar()
max_sync_cycles_spinner = ttk.Spinbox(self, format="%.0f cycles", textvariable=self.__max_sync_cycles_str, command=crosscheck_based_on_max)
max_sync_cycles_spinner["from"] = 2
max_sync_cycles_spinner["to"] = 10000000
max_sync_cycles_spinner.set("100 cycles")
max_sync_cycles_spinner.grid(row=4, column=1)
# Target variance
self.__target_variance_str = StringVar()
target_variance_spinner = ttk.Spinbox(self, format="%.0f ns^2", textvariable=self.__target_variance_str)
target_variance_spinner["from"] = 10
target_variance_spinner["to"] = 10**18
target_variance_spinner["increment"] = 10
target_variance_spinner.set("2500 ns^2")
target_variance_spinner.grid(row=5, column=1)
# Averaging window size
self.__averaging_window_size_str = StringVar()
averaging_window_size_spinner = ttk.Spinbox(self, format="%.0f samples", textvariable=self.__averaging_window_size_str)
averaging_window_size_spinner["from"] = 1
averaging_window_size_spinner["to"] = 10000
averaging_window_size_spinner["increment"] = 1
averaging_window_size_spinner.set("10 samples")
averaging_window_size_spinner.grid(row=6, column=1)
# ----------
# apply styling
for widget in self.winfo_children():
match widget:
case ttk.Label():
widget.configure(style="Parameters.TLabel")
widget.grid(sticky=tk.E)
def __init__(self, master: tk.Misc | None = None) -> None:
super().__init__(master)
self.__init_styles()
self.__init_layout()
self.__init_widgets()
self.configure(labelwidget=ttk.Label(self, text="Parameters", font=gcm.TITLE_FONT))
def get_parameters(self) -> dict:
return {
"linuxptp_init_timeout": int(self.__linuxptp_init_timeout_str.get()[:-1]),
"bmca_timeout": int(self.__bmca_timeout_str.get()[:-1]),
"sync_timeout": int(self.__sync_timeout_str.get()[:-6]),
"sync_min_cycles": int(self.__min_sync_cycles_str.get()[:-6]),
"sync_max_cycles": int(self.__max_sync_cycles_str.get()[:-6]),
"target_variance": int(self.__target_variance_str.get()[:-4]),
"averaging_window_size": int(self.__averaging_window_size_str.get()[:-7]),
}
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", "comments"), 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.heading("comments", text="Comments")
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_params_panel = TestParametersPanel(self)
self.__test_params_panel.grid(row=0, rowspan=3, column=2, sticky=tk.NSEW, ipadx=4, ipady=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 get_parameters(self) -> dict:
return self.__test_params_panel.get_parameters()
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, self.__test_params_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")