GUI class separation

This commit is contained in:
András Wiesner 2026-05-07 10:42:36 +02:00
parent cdbec108cb
commit 3b4976155c
8 changed files with 1102 additions and 374 deletions

View File

@ -29,11 +29,14 @@ class DeviceInterface:
os.write(self.__outpipe_rw_fd[1], data.encode()) os.write(self.__outpipe_rw_fd[1], data.encode())
if self.__out_cb is not None: # invoke callback if provided if self.__out_cb is not None: # invoke callback if provided
self.__out_cb(data) self.__out_cb(data, "")
except: except:
break # handle closing the device break # handle closing the device
if self.__out_cb is not None: # invoke callback if provided
self.__out_cb("", "exit")
def __init__(self, url: str, options: dict = {}) -> None: def __init__(self, url: str, options: dict = {}) -> None:
""" """
@ -141,7 +144,7 @@ class DeviceInterface:
timeout = True timeout = True
if separate_results: # separation requested if separate_results: # separation requested
records = re.findall("^[ ]*([^:]+)[ ]*:[ ]*(.+)[ ]*$", results.strip(), flags=re.MULTILINE) records = re.findall(r"^[ ]*([^:]+)[ ]*:[ ]*(.+)[ ]*$", results.strip(), flags=re.MULTILINE)
results = dict[str, str]() results = dict[str, str]()
for rec in records: for rec in records:
results[rec[0]] = rec[1].strip() results[rec[0]] = rec[1].strip()

View File

@ -1,8 +1,88 @@
import os
import queue
import re
import time
from typing import Callable
from DeviceInterface import DeviceInterface from DeviceInterface import DeviceInterface
class FlexPtpEvent:
def __init__(self, name: str, args: dict) -> None:
self.__name = name
self.__args = args
def get_name(self) -> str:
return self.__name
def get_args(self) -> dict:
return self.__args
class FlexPtpController: class FlexPtpController:
def __log_processor(self, data: str, spec_event: str) -> None:
if spec_event == "exit":
event = FlexPtpEvent("exit", {})
self.__event_queue.put(event)
return
# -------
stripped_data = data.strip()
line = stripped_data # strip accidental whitespaces from the start and the end of the line
match = re.search(r"^\[LOG-([A-Z:]+)\]", line) # serach for the LOGID
if match is not None and len(match.groups()) == 1: # check if LOGID was present
log_id = match.group(1) # extract LOGID
rest = line[len(match.group(0)):].strip()
if log_id == "BMCA": # BMCA event has occurred
match = re.match(r"([A-Z_]+) -> ([A-Z_]+)", rest)
if match is not None and len(match.groups()) == 2:
event = FlexPtpEvent("bmca-statechange", { "from": match.group(1), "to": match.group(2) })
self.__event_queue.put(event)
elif log_id == "DEF:S:A" or log_id == "DEF:S:H": # DEFAULT SLAVE log event has occurred
if_type = log_id[-1]
if if_type == "H": # HLT interface columns
cn = 10
else: # Addend interface columns
cn = 11
match = re.match(r"^" + r"([-.0-9]+)[ ]+" * (cn - 1) + r"([-.0-9]+)", rest)
if match is not None and len(match.groups()) == cn:
args = {
"T1": float(match.group(1) + "." + match.group(2)),
"T4": float(match.group(3) + "." + match.group(4)),
"Dt": int(match.group(5)) * 1000000000 + int(match.group(6)),
"corr_ppb": float(match.group(cn - 2)),
"mpd_ns": int(match.group(cn - 1)),
"sync_period_ns": int(match.group(cn))
}
self.__event_queue.put(FlexPtpEvent("synclog-slave", args=args))
elif log_id == "DEF:M:M": # DEFAULT master : mean path delay log event has occurred
self.__event_queue.put(FlexPtpEvent("mpd-master", { "mpd_ns": int(rest) }))
elif log_id == "DEF:M:S": # DEFAULT master : P2P slave state change log event has occurred
match = re.match(r"([A-Z_]+) -> ([A-Z_]+)", rest)
if match is not None and len(match.groups()) == 2:
event = FlexPtpEvent("p2p-slave-statechange", { "from": match.group(1), "to": match.group(2) })
self.__event_queue.put(event)
else:
# unhandled event
pass
if self.__log_cb is not None:
self.__log_cb(stripped_data + "\n")
def __init__(self, di: DeviceInterface) -> None: def __init__(self, di: DeviceInterface) -> None:
self.__di = di self.__di = di
self.__di.register_out_callback(self.__log_processor)
self.__log_cb: Callable | None = None
self.__event_queue = queue.Queue()
pass pass
def reset_flexptp(self) -> None: def reset_flexptp(self) -> None:
@ -47,9 +127,57 @@ class FlexPtpController:
self.__di.execute_command("ptp domain {:d}".format(domain)) self.__di.execute_command("ptp domain {:d}".format(domain))
def disable_all_logging(self) -> None: def disable_all_logging(self) -> None:
logging_types = [ "def", "corr", "ts", "info", "locked", "bmca" ] logging_types = [ "def", "corr", "ts", "info", "locked", "bmca", "logid" ]
for lt in logging_types: for lt in logging_types:
self.__di.execute_command("ptp log " + lt + " off", expect_results=False) self.__di.execute_command("ptp log " + lt + " off", expect_results=False)
def enable_log(self, id: str, en: bool) -> None:
if en:
onoff = "on"
else:
onoff = "off"
self.__di.execute_command("ptp log {:s} {:s}".format(id, onoff), expect_results=False)
def set_servo_offset(self, offset: int) -> None: def set_servo_offset(self, offset: int) -> None:
self.__di.execute_command("ptp servo offset {:d}".format(offset)) self.__di.execute_command("ptp servo offset {:d}".format(offset))
def register_log_callback(self, cb: Callable | None) -> None:
self.__log_cb = cb
def get_event(self, timeout: float) -> FlexPtpEvent:
try:
return self.__event_queue.get(timeout=timeout)
except:
return FlexPtpEvent("none", {})
def wait_for_event(self, expected_name: str, timeout: float, argcrits = {}) -> FlexPtpEvent:
timeout_left = timeout
start = time.time_ns()
event = FlexPtpEvent("none", {})
critera_met = False
while (event.get_name() != expected_name or not critera_met) and timeout_left > 0:
try:
event = self.__event_queue.get(timeout=timeout_left) # type: FlexPtpEvent
# propagate exit event
if event.get_name() == "exit":
return event
critera_met = True
for c_name, c_value in argcrits.items():
args = event.get_args()
if c_name in args:
if args[c_name] != c_value:
critera_met = False
break
except:
pass
now = time.time_ns()
timeout_left = timeout - ((now - start) / 1E+09)
return event

654
GUI.py
View File

@ -1,5 +1,10 @@
import queue import queue
import select
import sys
import threading
import time
from tkinter import Tk, font, ttk from tkinter import Tk, font, ttk
import numpy
import serial import serial
import serial.tools.list_ports import serial.tools.list_ports
import ttk_text as ttkt import ttk_text as ttkt
@ -7,344 +12,104 @@ from ttkthemes import ThemedTk
import tkinter as tk import tkinter as tk
import netifaces import netifaces
from matplotlib.backend_bases import key_press_handler
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from DeviceInterface import DeviceInterface from DeviceInterface import DeviceInterface
from FlexPtpController import FlexPtpController from FlexPtpController import FlexPtpController
from LinuxPtpObserver import LinuxPtpObserver from LinuxPtpObserver import LinuxPtpObserver
from gui.LoggingTab import LoggingTab
from gui.SetupTab import SetupTab
class GUI: class GUI:
def __init_flexPtp_options(self) -> None: def __start_stop_tests(self) -> str:
title = ttk.Label(self.__setup_tab, text="flexPTP options", font=self.__title_font)
frame = ttk.LabelFrame(self.__setup_tab, labelwidget=title)
frame.grid(row=0, column=0, sticky=tk.NSEW, ipady=4, padx=4)
#frame.pack(anchor="nw", fill="x", padx=12, ipady=4, side=tk.LEFT, expand=True)
frame.rowconfigure([0, 1, 2, 3 ], weight=1)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=3)
self.__flexPtp_options_frame = frame
device_label = ttk.Label(frame, text="Device:")
device_label.grid(column=0, row=0, sticky=tk.E)
baudrate_label = ttk.Label(frame, text="Baudrate:")
baudrate_label.grid(column=0, row=1, sticky=tk.E)
baudrate_label = ttk.Label(frame, text="Parity:")
baudrate_label.grid(column=0, row=2, sticky=tk.E)
baudrate_label = ttk.Label(frame, text="Stopbits:")
baudrate_label.grid(column=0, row=3, sticky=tk.E)
self.__sel_device = tk.StringVar()
device_combobox = ttk.Combobox(frame, textvariable=self.__sel_device)
device_combobox["values"] = list(map(lambda p: p.device, filter(lambda p: p.subsystem == "usb", serial.tools.list_ports.comports())))
device_combobox.current(0)
device_combobox.grid(column=1, row=0, sticky=tk.EW, padx=4)
#device_entry = ttk.Entry(frame, font=self.__console_font, textvariable=self.__sel_device)
#device_entry.grid(column=1, row=0, sticky=tk.EW, padx=4)
self.__sel_baudrate = tk.IntVar()
baudrate_combobox = ttk.Combobox(frame, textvariable=self.__sel_baudrate)
baudrate_combobox["values"] = (115200, 57600, 38400, 19200, 9600, 4800)
baudrate_combobox.current(0)
baudrate_combobox.grid(column=1, row=1, sticky=tk.EW, padx=4)
self.__sel_parity = tk.StringVar()
self.__sel_parity.set(serial.PARITY_NONE)
parity_frame = tk.Frame(frame)
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)
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)
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)
parity_odd.pack(anchor=tk.W, side=tk.LEFT)
self.__sel_stopbits = tk.IntVar()
self.__sel_stopbits.set(1)
stopbits_frame = tk.Frame(frame)
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_linuxptp_options(self) -> None:
title = ttk.Label(self.__setup_tab, text="linuxptp options", font=self.__title_font)
frame = ttk.LabelFrame(self.__setup_tab, labelwidget=title)
frame.grid(row=1, rowspan=1, column=0, sticky=tk.NSEW, ipady=4, padx=4)
# frame.pack(anchor="nw", fill="x", ipady=4, padx=12, side=tk.LEFT, expand=True)
frame.rowconfigure([0, 1, 2], weight=1)
# frame.rowconfigure(2, weight=100)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=3)
self.__linuxptp_options_frame = frame
path_label = ttk.Label(frame, text="Path:")
path_label.grid(column=0, row=0, sticky=tk.E)
if_label = ttk.Label(frame, text="Interface:")
if_label.grid(column=0, row=1, sticky=tk.E)
arg_label = ttk.Label(frame, text="Arguments:")
arg_label.grid(column=0, row=2, sticky=tk.E)
self.__linuxptp_path = tk.StringVar()
self.__linuxptp_path.set("/usr/sbin/ptp4l")
path_entry = ttk.Entry(frame, font=self.__console_font, textvariable=self.__linuxptp_path)
path_entry.grid(column=1, row=0, sticky=tk.EW, padx=4)
self.__linuxptp_interface = tk.StringVar()
baudrate_combobox = ttk.Combobox(frame, textvariable=self.__linuxptp_interface)
baudrate_combobox["values"] = netifaces.interfaces()
baudrate_combobox.current(0)
baudrate_combobox.grid(column=1, row=1, sticky=tk.EW, padx=4)
self.__linuxptp_args = tk.StringVar()
self.__linuxptp_args.set("")
args_entry = ttk.Entry(frame, font=self.__console_font, textvariable=self.__linuxptp_args)
args_entry.grid(column=1, row=2, sticky=tk.EW, padx=4)
def __init_test_cases(self) -> None:
title = ttk.Label(self.__setup_tab, text="Test cases", font=self.__title_font)
frame = ttk.LabelFrame(self.__setup_tab, labelwidget=title)
frame.grid(row=0, rowspan=2, column=1, sticky=tk.NSEW, ipady=4, padx=4)
# frame.pack(anchor="nw", fill="x", ipady=4, padx=12, side=tk.LEFT, expand=True)
self.__test_cases_frame = frame
basic_modes_frame = ttk.LabelFrame(frame, text="Basic modes")
basic_modes_frame.rowconfigure([0], weight=2)
basic_modes_frame.rowconfigure([1, 2], weight=8)
basic_modes_frame.columnconfigure([0], weight=2)
basic_modes_frame.columnconfigure([1, 2], weight=8)
basic_modes_frame.pack(expand=False, fill="both", anchor=tk.W, side=tk.LEFT, padx=4, pady=4)
e2e_label = ttk.Label(basic_modes_frame, text="E2E", style="ProfileLabel.TLabel", padding=6)
e2e_label.grid(row=0, column=1)
p2p_label = ttk.Label(basic_modes_frame, text="P2P", style="ProfileLabel.TLabel")
p2p_label.grid(row=0, column=2)
l4_label = ttk.Label(basic_modes_frame, text="L4", style="ProfileLabel.TLabel", padding=10)
l4_label.grid(row=1, column=0)
l2_label = ttk.Label(basic_modes_frame, text="L2", style="ProfileLabel.TLabel")
l2_label.grid(row=2, column=0)
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,
},
}
self.__test_cases = {}
for name, pos in modes.items():
oframe = ttk.Frame(basic_modes_frame, width=100, height=100, style="ProfileSquare.TFrame")
oframe.grid(row=pos["row"], column=pos["col"], sticky=tk.NSEW)
oframe.pack_propagate(False)
iframe = tk.Frame(oframe)
iframe.place(relx=0.5, rely=0.5, anchor=tk.CENTER)
m = tk.BooleanVar()
m_chk = ttk.Checkbutton(iframe, text="Master", variable=m, style="ProfileChkBox.TCheckbutton")
m_chk.pack(anchor=tk.CENTER, side=tk.TOP)
s = tk.BooleanVar()
s_chk = ttk.Checkbutton(iframe, text="Slave ", variable=s, style="ProfileChkBox.TCheckbutton")
s_chk.pack(anchor=tk.CENTER, side=tk.BOTTOM)
self.__test_cases[name] = {
"type": "general",
"delmech": name[0:3].upper(),
"layer": name[4:6].upper(),
"master": m,
"slave": s
}
defined_profiles_frame = ttk.LabelFrame(frame, text="Defined profiles")
defined_profiles_frame.pack(expand=True, fill="both", anchor=tk.W, side=tk.LEFT, padx=4, pady=4)
defined_profiles = [ "gPTP" ]
ri = 0
for dprof in defined_profiles:
label = ttk.Label(defined_profiles_frame, text=dprof + ":", font=self.__profile_font)
label.grid(row=ri, column=0, padx=6)
m = tk.BooleanVar()
m_chk = ttk.Checkbutton(defined_profiles_frame, text="Master", variable=m, style="ProfileChkBox.TCheckbutton")
m_chk.grid(row=ri, column=1)
s = tk.BooleanVar()
s_chk = ttk.Checkbutton(defined_profiles_frame, text="Slave ", variable=s, style="ProfileChkBox.TCheckbutton")
s_chk.grid(row=ri, column=2)
self.__test_cases[dprof] = {
"type": "defined",
"master": m,
"slave": s
}
def __init_test_controller(self) -> None:
title = ttk.Label(self.__setup_tab, text="Test controls", font=self.__title_font)
frame = ttk.LabelFrame(self.__setup_tab, labelwidget=title)
frame.grid(row=2, column=0, columnspan=2, sticky=tk.NSEW, ipady=4, padx=4)
self.__tests_running = False
def start_stop_tests() -> None:
self.__start_stop_btn.configure(state="disabled")
if not self.__tests_running: if not self.__tests_running:
self.start_tests() if self.start_tests():
self.__tests_running = True
return "running"
else:
return "stopped"
else: else:
self.stop_tests() self.stop_tests()
self.__tests_running = False
self.__tests_running = not self.__tests_running return "stopped"
if self.__tests_running:
self.__start_stop_btn.configure(text="STOP")
state = "disabled"
else:
self.__start_stop_btn.configure(text="START")
state = "normal"
frames = [ self.__flexPtp_options_frame, self.__linuxptp_options_frame, self.__test_cases_frame ]
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)
for frame in frames:
set_widget_state(frame)
self.__start_stop_btn.configure(state="enabled")
self.__start_stop_btn = ttk.Button(frame, text="START", command=start_stop_tests)
self.__start_stop_btn.pack()
tw = ttk.Treeview(frame, 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("in_progress", background="Gold")
tw.tag_configure("pending", font=("TkDefaultFont", 9, "italic"), foreground="Gray")
tw.pack()
self.__test_tw = tw
def __init_setup_tab(self) -> None: def __init_setup_tab(self) -> None:
self.__setup_tab = ttk.Frame(self.__tabs) """
self.__setup_tab.columnconfigure(0, weight=1) Initialize "setup" tab widgets.
self.__setup_tab.columnconfigure(1, weight=4) """
self.__setup_tab.rowconfigure([0, 1], weight=1)
self.__setup_tab.rowconfigure(2, weight=100)
self.__tabs.add(self.__setup_tab, text="Setup")
self.__init_flexPtp_options() self.__setup_tab = SetupTab(self.__tabs)
self.__init_linuxptp_options() self.__setup_tab.register_start_stop_tests_cb(self.__start_stop_tests)
self.__init_test_cases()
self.__init_test_controller() self.__tabs.add(self.__setup_tab, text="Setup")
def __init_logtab(self) -> None: def __init_logtab(self) -> None:
self.__logtab = ttk.Frame(self.__tabs) """
self.__tabs.add(self.__logtab, text="Logs") Initialize log tab.
"""
self.__logtab.rowconfigure(0, weight=8) # initialize tab and its widgets
self.__logtab.rowconfigure(1, weight=2) self.__logtab = LoggingTab(self.__tabs)
self.__logtab.columnconfigure(0, weight=1)
self.__logtab.columnconfigure(1, weight=1)
terminal_settings = { "wrap": tk.WORD, "background": "#3D3D3D", "borderwidth": 0, "foreground": "white", "blockcursor": True, "insertbackground": "white"}
self.__flexPtp_log = tk.Text(self.__logtab, **terminal_settings)
self.__flexPtp_log.grid(column=0, row=0, sticky=tk.NSEW)
self.__flexPtp_log.configure(font=self.__console_font, state="disabled")
self.__linuxptp_log = tk.Text(self.__logtab, **terminal_settings)
self.__linuxptp_log.grid(column=1, row=0, sticky=tk.NSEW, pady=0)
self.__linuxptp_log.configure(font=self.__console_font, state="disabled")
# initialize messaging queues
self.__flexPtp_log_queue = queue.Queue() self.__flexPtp_log_queue = queue.Queue()
self.__linuxptp_log_queue = queue.Queue() self.__linuxptp_log_queue = queue.Queue()
def __init_test_results(self) -> None: def __init_results_tab(self) -> None:
"""
Initialize "test results" tab.
"""
self.__results_tabs = ttk.Frame(self.__tabs) self.__results_tabs = ttk.Frame(self.__tabs)
self.__tabs.add(self.__results_tabs, text="Results") self.__tabs.add(self.__results_tabs, text="Results")
fig = Figure(figsize=(5, 4), dpi=100)
t = numpy.arange(0, 3, .01)
ax = fig.add_subplot()
line, = ax.plot(t, 2 * numpy.sin(2 * numpy.pi * t))
ax.set_xlabel("time [s]")
ax.set_ylabel("f(t)")
canvas = FigureCanvasTkAgg(fig, master=self.__results_tabs) # A tk.DrawingArea.
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
def __init_tabs(self) -> None: def __init_tabs(self) -> None:
"""
Initialize tabs.
"""
self.__init_setup_tab() self.__init_setup_tab()
self.__init_logtab() self.__init_logtab()
self.__init_test_results() self.__init_results_tab()
def __init_statusbar(self) -> None:
self.__status = tk.StringVar(value="Idle")
statusbar = ttk.Label(self.__win, textvariable=self.__status)
statusbar.pack(anchor=tk.S, side=tk.BOTTOM, fill=tk.X)
def __init_fonts(self) -> None: def set_status(self, status: str) -> None:
self.__console_font = font.Font(family="Ubuntu Mono", size=9) self.__status.set(status)
self.__title_font=font.Font(family="Ubuntu", size=14)
self.__mode_font=font.Font(family="Ubuntu", size=12, weight="bold")
self.__profile_font=font.Font(family="Ubuntu", size=12, slant="italic")
def __init_styles(self) -> None:
style = ttk.Style()
style.configure("ProfileChkBox.TCheckbutton", font=self.__console_font)
style.configure("ProfileSquare.TFrame", bordercolor="#003039", borderwidth=1, relief="solid")
style.configure("ProfileLabel.TLabel", font=self.__mode_font)
def __init_print_polls(self) -> None: def __init_print_polls(self) -> None:
"""
Initialize polled log printing.
"""
def print_logs() -> None: def print_logs() -> None:
while not self.__flexPtp_log_queue.empty(): while not self.__flexPtp_log_queue.empty():
self.__flexPtp_log.configure(state="normal") self.__logtab.add_log("flexPTP", self.__flexPtp_log_queue.get())
self.__flexPtp_log.insert(tk.END, self.__flexPtp_log_queue.get())
self.__flexPtp_log.configure(state="disabled")
while not self.__linuxptp_log_queue.empty(): while not self.__linuxptp_log_queue.empty():
self.__linuxptp_log.configure(state="normal") self.__logtab.add_log("linuxptp", self.__linuxptp_log_queue.get())
self.__linuxptp_log.insert(tk.END, self.__linuxptp_log_queue.get())
self.__linuxptp_log.configure(state="disabled")
self.__win.after(50, print_logs) self.__win.after(50, print_logs)
@ -353,38 +118,45 @@ class GUI:
def __init__(self) -> None: def __init__(self) -> None:
self.__win = ThemedTk(theme="arc") self.__win = ThemedTk(theme="arc")
self.__win.minsize(1000, 700)
#self.__win.configure() #self.__win.configure()
self.__win.title("flexPTP test suite") self.__win.title("flexPTP test suite")
self.__win.iconphoto(False, tk.PhotoImage(file="media/flexPTP_test.png")) self.__win.iconphoto(False, tk.PhotoImage(file="media/flexPTP_test.png"))
self.__init_fonts()
self.__init_styles()
self.__tabs = ttk.Notebook(self.__win) self.__tabs = ttk.Notebook(self.__win)
self.__tabs.configure(width=1000, height=600) self.__tabs.configure()
self.__init_tabs() self.__init_tabs()
self.__tabs.pack(expand=True, anchor=tk.S, side=tk.TOP, fill=tk.BOTH)
self.__tabs.pack(expand=True, fill="both") self.__init_statusbar()
self.__init_print_polls() self.__init_print_polls()
self.__tests_running = False
self.__user_stop = False
def mainloop(self) -> None: def mainloop(self) -> None:
self.__win.mainloop() self.__win.mainloop()
def __gather_tests(self) -> None: def __gather_tests(self) -> None:
# gather test cases """
Gather user-selected test cases.
"""
self.__tests = [] self.__tests = []
for name, case in self.__test_cases.items(): for name, case in self.__setup_tab.get_test_cases().items():
ptype = case["type"] ptype = case["type"]
template = {} template = {}
if ptype == "general": if ptype == "general":
template = { "type": ptype, "name": case["delmech"] + " " + case["layer"], "delmech": case["delmech"], "layer": case["layer"] } id = case["delmech"] + "_" + case["layer"]
name = case["delmech"] + " " + case["layer"]
template = { "type": ptype, "name": name, "id": id, "delmech": case["delmech"], "layer": case["layer"] }
elif ptype == "defined": elif ptype == "defined":
template = { "type": ptype, "name": name } template = { "type": ptype, "name": name, "id": name }
if case["master"].get(): if case["master"].get():
master_mode = dict(template) master_mode = dict(template)
@ -400,7 +172,11 @@ class GUI:
def __populate_tests_treeview(self) -> None: def __populate_tests_treeview(self) -> None:
tw = self.__test_tw """
Fill the test treeview.
"""
tw = self.__setup_tab.get_test_treeview()
items = tw.get_children() items = tw.get_children()
if items != (): if items != ():
tw.delete(*items) tw.delete(*items)
@ -413,71 +189,267 @@ class GUI:
def __open_flexPtp_interface(self) -> None: def __open_flexPtp_interface(self) -> None:
url = self.__sel_device.get() """
Open the flexPTP device interface.
"""
flexPtp_settings = self.__setup_tab.get_settings()["flexPTP"]
device = flexPtp_settings["device"]
opts = { opts = {
"baudrate": self.__sel_baudrate.get(), "baudrate": flexPtp_settings["baudrate"],
"parity": self.__sel_parity.get(), "parity": flexPtp_settings["parity"],
"stopbits": self.__sel_stopbits.get() "stopbits": flexPtp_settings["stopbits"]
} }
def echo(data: str) -> None: self.__device_interface = DeviceInterface(device, opts)
self.__flexPtp_log_queue.put(data.replace("\r", ""))
self.__device_interface = DeviceInterface(url, opts)
self.__device_interface.register_out_callback(echo)
self.__flexPtp_controller = FlexPtpController(self.__device_interface) self.__flexPtp_controller = FlexPtpController(self.__device_interface)
def __init_linuxptp_observer(self) -> None: def __init_linuxptp_observer(self) -> None:
self.__linuxptp_observer = LinuxPtpObserver(self.__linuxptp_interface.get()) """
Initialize the linuxptp observer.
"""
def echo(data) -> None: linuxptp_settings = self.__setup_tab.get_settings()["linuxptp"]
self.__linuxptp_log_queue.put(str(data))
self.__linuxptp_observer.register_observer_callback(echo)
self.__linuxptp_observer = LinuxPtpObserver(linuxptp_settings["interface"])
def __init_flexPtp(self) -> None: def __init_flexPtp(self) -> None:
"""
Initialize flexPTP.
"""
ctrl = self.__flexPtp_controller ctrl = self.__flexPtp_controller
ctrl.disable_all_logging() ctrl.disable_all_logging()
ctrl.set_domain(0) ctrl.set_domain(0)
ctrl.reset_flexptp() ctrl.reset_flexptp()
ctrl.enable_log("logid", True)
def test_routine(self) -> None:
LINUXPTP_INIT_TIMEOUT = 5
BMCA_TIMEOUT = 150
SYNC_TIMEOUT = 4
SYNC_MAX_CYCLES = 100
TARGET_RANGE = 50**2
WINDOW_SIZE = 10
def start_tests(self) -> None: def flexPtp_echo(data: str) -> None:
self.__gather_tests() self.__flexPtp_log_queue.put(str(data))
self.__populate_tests_treeview()
self.__open_flexPtp_interface()
self.__init_flexPtp()
self.__init_linuxptp_observer()
# ---- def linuxptp_echo(data: str) -> None:
self.__linuxptp_log_queue.put(str(data))
def both_echo(data: str) -> None:
flexPtp_echo(data)
linuxptp_echo(data)
self.__flexPtp_controller.register_log_callback(flexPtp_echo)
self.__linuxptp_observer.register_observer_callback(linuxptp_echo)
def time_to_str(t: float) -> str:
return time.strftime("%d-%m-%Y %H:%M:%S", time.gmtime(t))
ctrl = self.__flexPtp_controller ctrl = self.__flexPtp_controller
observer = self.__linuxptp_observer observer = self.__linuxptp_observer
for test in self.__tests:
if test["type"] == "general":
id = test["delmech"] + "_" + test["layer"]
else:
id = test["name"]
tests_errored = False
tsn = 1
for test in self.__tests:
# identify correct mode or profile
id = test["id"]
mode = test["mode"]
# set status
self.set_status("[{:d}/{:d}] Test in progress: {:s}".format(tsn, len(self.__tests), test["name"]))
# setup info variables for the test
test["start"] = time.time()
str_start_time = time_to_str(test["start"])
values = (test["name"], "In-progress", str_start_time, "-", "-")
self.__setup_tab.get_test_treeview().item(test["entry"], values=values, tags=("in_progress"))
# announce the start of a new testcase
both_echo("\n\n----------\nSTARTING test: {:s} ({:s})\n start time: {:s}\n----------\n\n".format(id, mode, str_start_time))
# set mode or profile on the flexPTP device
ctrl.start_by_id(id) ctrl.start_by_id(id)
# adjust device priority to force required working mode
priority1 = 128 priority1 = 128
if test["mode"] == "master": if mode == "master":
priority1 = 100 priority1 = 100
ctrl.set_priority(priority1, 255) ctrl.set_priority(priority1, 255)
# start linuxptp with the matching mode or profile
observer.start_linuxptp(id) observer.start_linuxptp(id)
n = 0
var = 1E+09
dts = []
try:
# wait for the LISTENING state clearly indicating the startup of the linuxptp
event = observer.wait_for_event("bmca-statechange", LINUXPTP_INIT_TIMEOUT, { "to": "LISTENING" })
if event.get_name() != "bmca-statechange":
raise Exception("linuxptp init failed")
# turn on BMCA and default logging on the flexPTP device
ctrl.enable_log("bmca", True)
ctrl.enable_log("def", True)
# assign flexPTP and linuxptp sides
if mode == "slave":
master_side = observer
slave_side = ctrl
observer_bmca_role = "MASTER"
ctrl_bmca_role = "SLAVE"
else:
master_side = ctrl
slave_side = observer
observer_bmca_role = "SLAVE"
ctrl_bmca_role = "MASTER"
for test in self.__tests: # wait for the correct BMCA states to settle in
pass error = False
error_cause = "none"
event = ctrl.wait_for_event("bmca-statechange", BMCA_TIMEOUT, { "to": ctrl_bmca_role }) # flexPTP
if event.get_name() != "bmca-statechange":
raise Exception(event.get_name())
event = observer.wait_for_event("bmca-statechange", BMCA_TIMEOUT, { "to": observer_bmca_role }) # linuxptp
if event.get_name() != "bmca-statechange":
raise Exception(event.get_name())
# wait for the synchronization to settle
while n < SYNC_MAX_CYCLES and var > TARGET_RANGE:
event = slave_side.get_event(SYNC_TIMEOUT)
# a state change with the BMCA indicates a clear error
if event.get_name() == "synclog-slave":
dts.append(event.get_args()["Dt"])
elif event.get_name() in ("bmca-statechange", "exit"):
raise Exception(event.get_name())
if n >= WINDOW_SIZE:
var = numpy.var(dts[n-10:n])
n += 1
# check variance
error = not var < TARGET_RANGE
except Exception as e:
error = True
error_cause = e.args[0]
# display linuxptp errors
linuxptp_errors = observer.get_errors()
if linuxptp_errors != "":
linuxptp_errors = "ERROR:\n" + linuxptp_errors
linuxptp_echo(linuxptp_errors)
# stop linuxptp
observer.stop_linuxptp()
# exit if some of the subordinate layers signaled an exit
if error and error_cause in ("exit", "linuxptp init failed"):
self.set_status("Tests stopped")
tests_errored = True
# save results, conclusion time and calculate duration
test["dts"] = dts
test["cycles"] = n
passed = not error
if passed:
test["result"] = "passed"
else:
if tests_errored:
test["result"] = "errored"
else:
test["result"] = "failed"
# save timestamps
test["end"] = time.time()
test["duration"] = test["end"] - test["start"]
str_end_time = time_to_str(test["end"])
str_duration = time.strftime("%H:%M:%S", time.gmtime(test["duration"]))
self.__setup_tab.get_test_treeview().item(test["entry"], values=(
test["name"], test["result"].upper(),
str_start_time,
str_end_time,
str_duration
), tags=(test["result"]))
# announce the end of the test
both_echo("\n\n----------\nCONCLUDING test: {:s} ({:s})\n result: {:s}\n start time: {:s}\n stop time: {:s}\n duration: {:s}\n----------\n\n".format(
id, mode, test["result"].upper(), str_start_time, str_end_time, str_duration))
if tests_errored:
break
tsn += 1
if not self.__user_stop:
self.stop_tests()
if not tests_errored:
self.set_status("Tests concluded")
self.__user_stop = False
def stop_tests(self) -> None: def start_tests(self) -> bool:
"""
Start selected tests.
"""
self.__gather_tests()
if len(self.__tests) == 0:
self.set_status("No tests have been selected")
return False
self.__populate_tests_treeview()
self.set_status("Preparing flexPTP device")
self.__open_flexPtp_interface()
self.__init_flexPtp()
self.set_status("Starting linuxptp observer")
self.__init_linuxptp_observer()
# ----
self.set_status("Starting tests")
self.__test_thread = threading.Thread(target=self.test_routine)
self.__test_thread.start()
self.__setup_tab.set_test_state("running")
return True
def stop_tests(self) -> bool:
"""
Stop tests in progress.
"""
self.__user_stop = True
self.__linuxptp_observer.stop_linuxptp() self.__linuxptp_observer.stop_linuxptp()
self.__device_interface.close() self.__device_interface.close()
self.__setup_tab.set_test_state("stopped")
return True

View File

@ -1,42 +1,101 @@
import queue
import signal import signal
import subprocess import subprocess
import select import select
from threading import Thread from threading import Thread
import time
from typing import Callable from typing import Callable
import re
class LinuxPtpEvent:
def __init__(self, name: str, args: dict) -> None:
self.__name = name
self.__args = args
def get_name(self) -> str:
return self.__name
def get_args(self) -> dict:
return self.__args
class LinuxPtpObserver: class LinuxPtpObserver:
OFFSET_REGEX = r"ptp4l\[[0-9.]+\]: master offset[ ]+([-\d]+) s\d freq[ ]+([-\d]+) path delay[ ]+([\d]+)"
BMCA_REGEX = r"ptp4l\[[0-9.]+\]: port \d+ \([a-z0-9]+\): ([A-Z_]+) to ([A-Z_]+) on [A-Z_]+"
def __init__(self, ni: str) -> None: def __init__(self, ni: str) -> None:
self.__ni = ni self.__ni = ni
self.__process: subprocess.Popen self.__process: subprocess.Popen
self.__observer_thread: Thread self.__observer_thread: Thread
self.__observer_cb: Callable | None = None self.__observer_cb: Callable | None = None
self.__event_queue = queue.Queue()
self.__error_queue = queue.Queue()
pass pass
def observer_routine(self) -> None: def observer_routine(self) -> None:
if self.__process.stdout is not None:
pfd = select.poll() pfd = select.poll()
if self.__process.stdout is not None:
pfd.register(self.__process.stdout.fileno(), select.POLLIN) pfd.register(self.__process.stdout.fileno(), select.POLLIN)
else: else:
return return
if self.__process.stderr is not None:
pfd.register(self.__process.stderr.fileno(), select.POLLIN)
else:
return
while self.__process.poll() is None: while self.__process.poll() is None:
res = pfd.poll() res = pfd.poll()
if len(res) > 0 and res[0][1] & select.POLLIN: for fde in res:
data = self.__process.stdout.readline() if fde[0] == self.__process.stdout.fileno() and fde[1] & select.POLLIN: # STDOUT
if self.__observer_cb is not None: orig_data = self.__process.stdout.readline()
self.__observer_cb(data) data = orig_data.strip()
# SYNCLOG lines
match = re.match(LinuxPtpObserver.OFFSET_REGEX, data)
if match is not None:
args = {
"Dt": int(match.group(1)),
"mpd_ns": int(match.group(3))
}
event = LinuxPtpEvent("synclog-slave", args)
self.__event_queue.put(event)
# BMCA state change lines
match = re.match(LinuxPtpObserver.BMCA_REGEX, data)
if match is not None:
args = {
"from": match.group(1),
"to": match.group(2)
}
event = LinuxPtpEvent("bmca-statechange", args)
self.__event_queue.put(event)
if self.__observer_cb is not None:
self.__observer_cb(orig_data)
elif fde[0] == self.__process.stderr.fileno() and fde[1] & select.POLLIN: # STDERR
text = self.__process.stderr.read()
self.__error_queue.put(text)
event = LinuxPtpEvent("error", { "text": text })
self.__event_queue.put(event)
# thread has returned
event = LinuxPtpEvent("exit", {})
self.__event_queue.put(event)
def start_linuxptp(self, profile: str) -> None: def start_linuxptp(self, profile: str) -> None:
cmd = ["sudo", "ptp4l", "--priority1=127", "--priority2=255", "--gmCapable=1", "--neighborPropDelayThresh=100000", cmd = ["sudo", "ptp4l", "--priority1=127", "--priority2=255", "--gmCapable=1", "--neighborPropDelayThresh=100000",
"--min_neighbor_prop_delay=-20000000", "--assume_two_step=1", "--ptp_minor_version=0", "--min_neighbor_prop_delay=-20000000", "--assume_two_step=1", "--ptp_minor_version=0", "--summary_interval=-5",
"-i", self.__ni, "-f", "linuxptp_configs/{:s}.cfg".format(profile), "-m", "-l", "6"] "-i", self.__ni, "-f", "linuxptp_configs/{:s}.cfg".format(profile), "-m", "-l", "6"]
self.__process = subprocess.Popen( self.__process = subprocess.Popen(
cmd, cmd,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stdin=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True text=True
) )
@ -46,6 +105,51 @@ class LinuxPtpObserver:
def stop_linuxptp(self) -> None: def stop_linuxptp(self) -> None:
self.__process.send_signal(signal.SIGKILL) self.__process.send_signal(signal.SIGKILL)
self.__observer_thread.join() self.__observer_thread.join()
self.__event_queue = queue.Queue()
def register_observer_callback(self, cb : Callable) -> None: def register_observer_callback(self, cb : Callable) -> None:
self.__observer_cb = cb self.__observer_cb = cb
def get_event(self, timeout: float) -> LinuxPtpEvent:
try:
return self.__event_queue.get(timeout=timeout)
except:
return LinuxPtpEvent("none", {})
def wait_for_event(self, expected_name: str, timeout: float, argcrits = {}) -> LinuxPtpEvent:
timeout_left = timeout
start = time.time_ns()
event = LinuxPtpEvent("none", {})
critera_met = False
while (event.get_name() != expected_name or not critera_met) and timeout_left > 0:
try:
event = self.__event_queue.get(timeout=timeout_left) # type: LinuxPtpEvent
# propagate exit event
if event.get_name() in ("exit", "error"):
return event
critera_met = True
for c_name, c_value in argcrits.items():
args = event.get_args()
if c_name in args:
if args[c_name] != c_value:
critera_met = False
break
except:
pass
now = time.time_ns()
timeout_left = timeout - ((now - start) / 1E+09)
return event
def get_errors(self) -> str:
error = ""
while not self.__error_queue.empty():
error += self.__error_queue.get()
return error

3
gui/GuiCommon.py Normal file
View File

@ -0,0 +1,3 @@
TITLE_FONT = ("Ubuntu", 14) # frame title font
TERMINAL_FONT = ("Ubuntu Mono", 9) # terminal font
MODE_FONT = ("Ubuntu", 12, "bold") # mode font

41
gui/LoggingTab.py Normal file
View File

@ -0,0 +1,41 @@
import tkinter as tk
from tkinter import ttk
from typing import Literal
from gui.TerminalDisplay import TerminalDisplay
class LoggingTab(ttk.Frame):
def __init_layout(self) -> None:
self.rowconfigure(0, weight=8)
self.rowconfigure(1, weight=2)
self.columnconfigure(0, weight=1)
self.columnconfigure(1, weight=1)
def __init_widgets(self) -> None:
self.__flexPtp_log = TerminalDisplay(self)
self.__flexPtp_log.grid(column=0, row=0, sticky=tk.NSEW)
self.__linuxptp_log = TerminalDisplay(self)
self.__linuxptp_log.grid(column=1, row=0, sticky=tk.NSEW)
def __init__(self, notebook: ttk.Notebook) -> None:
super().__init__(master=notebook)
notebook.add(self, text="Logs")
self.__init_layout() # initialize widget layout
self.__init_widgets() # initialize widgets
def clear_log(self, which: Literal["flexPTP", "linuxptp", "both"]) -> None:
if which in ("flexPTP", "both"):
self.__flexPtp_log.delete(1, tk.END)
if which in ("linuxptp", "both"):
self.__linuxptp_log.delete(1, tk.END)
def add_log(self, which: Literal["flexPTP", "linuxptp", "both"], what: str) -> None:
if which in ("flexPTP", "both"):
self.__flexPtp_log.insert(tk.END, what)
if which in ("linuxptp", "both"):
self.__linuxptp_log.insert(tk.END, what)

446
gui/SetupTab.py Normal file
View File

@ -0,0 +1,446 @@
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")

31
gui/TerminalDisplay.py Normal file
View File

@ -0,0 +1,31 @@
from _tkinter import Tcl_Obj
import tkinter as tk
import gui.GuiCommon as gcm
class TerminalDisplay(tk.Text):
# global terminal-style outlook
TERMINAL_STYLE = {
"wrap": tk.WORD,
"background": "#3D3D3D",
"borderwidth": 0,
"foreground": "white",
"blockcursor": True,
"insertbackground": "white"
}
def __init__(self, master: tk.Widget) -> None:
super().__init__(master=master, **self.TERMINAL_STYLE, font=gcm.TERMINAL_FONT, state=tk.DISABLED)
def insert(self, index: str | float | Tcl_Obj | tk.Widget, chars: str, *args: str | list[str] | tuple[str, ...]) -> None:
self.configure(state=tk.NORMAL)
ret = super().insert(index, chars, *args)
self.yview(tk.END)
self.configure(state=tk.DISABLED)
return ret
def delete(self, index1: str | float | Tcl_Obj | tk.Widget, index2: str | float | Tcl_Obj | tk.Widget | None = None) -> None:
self.configure(state=tk.NORMAL)
ret = super().delete(index1, index2)
self.configure(state=tk.DISABLED)
return ret