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: ) 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")