This commit is contained in:
András Wiesner 2026-04-27 21:47:57 +02:00
parent df1d6a9f1c
commit cdbec108cb
16 changed files with 888 additions and 61 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__/

2
.vscode/launch.json vendored
View File

@ -12,8 +12,10 @@
"program": "${workspaceFolder}/test_main.py", "program": "${workspaceFolder}/test_main.py",
"python": ".venv/bin/python", "python": ".venv/bin/python",
"console": "integratedTerminal", "console": "integratedTerminal",
"justMyCode": false,
"args": [ "args": [
"/dev/ttyACM0", "/dev/ttyACM0",
"enp52s0f0"
] ]
} }
] ]

View File

@ -1,11 +1,40 @@
import sys, os
import select
from typing import Callable
import serial import serial
import re import re
from threading import Thread, Lock
class DeviceInterface: class DeviceInterface:
""" """
Helper class for interfacing the device running flexPTP. Helper class for interfacing the device running flexPTP.
""" """
def reader_routine(self) -> None:
"""
Thread routine for receiving all incoming communication from
the device.
"""
while not self.device.closed:
try:
data = self.device.readline() # read serial port line-by-line
if type(data) is bytes:
# remove colorization escape sequences
data = re.sub(r"\x1b\[[0-9;]*m", "", data.decode())
if len(data) > 0: # write (remaining) data to the pipe
os.write(self.__outpipe_rw_fd[1], data.encode())
if self.__out_cb is not None: # invoke callback if provided
self.__out_cb(data)
except:
break # handle closing the device
def __init__(self, url: str, options: dict = {}) -> None: def __init__(self, url: str, options: dict = {}) -> None:
""" """
Initialize the interface. Initialize the interface.
@ -15,7 +44,7 @@ class DeviceInterface:
:rtype: None :rtype: None
""" """
self.device = serial.serial_for_url(url, timeout=0.05) # open remote device self.device = serial.serial_for_url(url, timeout=None) # open remote device
# set serial port options if applicable # set serial port options if applicable
if "baudrate" in options: if "baudrate" in options:
@ -27,16 +56,38 @@ class DeviceInterface:
if "stopbits" in options: if "stopbits" in options:
self.device.stopbits = options["stopbits"] self.device.stopbits = options["stopbits"]
def read_until(self, expected: bytes = serial.LF) -> bytes: # ----
"""
Read from the device until a specific sequence is found. self.__out_cb: Callable | None = None
r, w = os.pipe()
self.__outpipe_rw_fd = [ r, w ]
self.__outpipe_rw = [ os.fdopen(r, "rb"), os.fdopen(w, "wb") ]
self.__reader_thread = Thread(target=self.reader_routine)
self.__reader_thread.start()
def close(self) -> None:
"""
Close the device interface.
:rtype: None
"""
self.device.close()
self.__reader_thread.join()
self.__outpipe_rw[0].close()
self.__outpipe_rw[1].close()
def readline(self) -> bytes:
"""
Read a line from the device.
:param bytes expected: delimiter sequence
:return: bytes read :return: bytes read
:rtype: bytes :rtype: bytes
""" """
return self.device.read_until(expected=expected) return self.__outpipe_rw[0].readline().encode()
def write(self, data: bytes) -> None: def write(self, data: bytes) -> None:
@ -60,15 +111,32 @@ class DeviceInterface:
:rtype: str | None :rtype: str | None
""" """
self.device.write((cmd.strip("\r\n") + "\r\n").encode()) # sanitize commands pure_final_cmd = cmd.strip("\r\n")
self.device.read_until(cmd.encode()) # flush echo final_cmd = (pure_final_cmd + "\r\n").encode() # sanitize commands
#self.skip(len(final_cmd)) # flush echo
self.write(final_cmd) # send command
# wait for the echo to arrive
line = ""
while line != pure_final_cmd:
line = self.__outpipe_rw[0].readline().decode().strip()
if expect_results: # store results if required if expect_results: # store results if required
timeout = False timeout = False
results = "" results = ""
poller = select.poll()
poller.register(self.__outpipe_rw_fd[0], select.POLLIN)
while not timeout: # store continuous chunks while not timeout: # store continuous chunks
data = self.device.read(32) res = poller.poll(100)
if len(data) > 0:
results += data.decode() if len(res) > 0 and res[0][1] == select.POLLIN:
data = self.__outpipe_rw[0].read(1)
if len(data) > 0:
results += data.decode()
else:
timeout = True
else: else:
timeout = True timeout = True
@ -82,3 +150,14 @@ class DeviceInterface:
return results return results
else: else:
return None return None
def register_out_callback(self, cb: Callable) -> None:
"""
Register callback for receiving all printed messages from the
device.
:param Callable cb: callback invoked at each reception
:rtype: None
"""
self.__out_cb = cb

55
FlexPtpController.py Normal file
View File

@ -0,0 +1,55 @@
from DeviceInterface import DeviceInterface
class FlexPtpController:
def __init__(self, di: DeviceInterface) -> None:
self.__di = di
pass
def reset_flexptp(self) -> None:
self.__di.execute_command("ptp reset")
def start_e2e_udp(self) -> None:
self.__di.execute_command("ptp profile preset default")
def start_p2p_udp(self) -> None:
self.__di.execute_command("ptp profile preset defp2p")
def start_e2e_l2(self) -> None:
self.__di.execute_command("ptp profile preset default")
self.__di.execute_command("ptp transport 802.3")
self.reset_flexptp()
def start_p2p_l2(self) -> None:
self.__di.execute_command("ptp profile preset defp2p")
self.__di.execute_command("ptp transport 802.3")
self.reset_flexptp()
def start_gPTP(self) -> None:
self.__di.execute_command("ptp profile preset gPTP")
def start_by_id(self, id: str) -> None:
id = id.replace("_", "").upper()
if id == "E2EL4":
self.start_e2e_udp()
elif id == "E2EL2":
self.start_e2e_l2()
elif id == "P2PL4":
self.start_p2p_udp()
elif id == "P2PL2":
self.start_p2p_l2()
elif id == "GPTP":
self.start_gPTP()
def set_priority(self, priority1: int, priority2: int) -> None:
self.__di.execute_command("ptp priority {:d} {:d}".format(priority1, priority2))
def set_domain(self, domain: int) -> None:
self.__di.execute_command("ptp domain {:d}".format(domain))
def disable_all_logging(self) -> None:
logging_types = [ "def", "corr", "ts", "info", "locked", "bmca" ]
for lt in logging_types:
self.__di.execute_command("ptp log " + lt + " off", expect_results=False)
def set_servo_offset(self, offset: int) -> None:
self.__di.execute_command("ptp servo offset {:d}".format(offset))

483
GUI.py Normal file
View File

@ -0,0 +1,483 @@
import queue
from tkinter import Tk, font, ttk
import serial
import serial.tools.list_ports
import ttk_text as ttkt
from ttkthemes import ThemedTk
import tkinter as tk
import netifaces
from DeviceInterface import DeviceInterface
from FlexPtpController import FlexPtpController
from LinuxPtpObserver import LinuxPtpObserver
class GUI:
def __init_flexPtp_options(self) -> None:
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:
self.start_tests()
else:
self.stop_tests()
self.__tests_running = not self.__tests_running
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:
self.__setup_tab = ttk.Frame(self.__tabs)
self.__setup_tab.columnconfigure(0, weight=1)
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.__init_linuxptp_options()
self.__init_test_cases()
self.__init_test_controller()
def __init_logtab(self) -> None:
self.__logtab = ttk.Frame(self.__tabs)
self.__tabs.add(self.__logtab, text="Logs")
self.__logtab.rowconfigure(0, weight=8)
self.__logtab.rowconfigure(1, weight=2)
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")
self.__flexPtp_log_queue = queue.Queue()
self.__linuxptp_log_queue = queue.Queue()
def __init_test_results(self) -> None:
self.__results_tabs = ttk.Frame(self.__tabs)
self.__tabs.add(self.__results_tabs, text="Results")
def __init_tabs(self) -> None:
self.__init_setup_tab()
self.__init_logtab()
self.__init_test_results()
def __init_fonts(self) -> None:
self.__console_font = font.Font(family="Ubuntu Mono", size=9)
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 print_logs() -> None:
while not self.__flexPtp_log_queue.empty():
self.__flexPtp_log.configure(state="normal")
self.__flexPtp_log.insert(tk.END, self.__flexPtp_log_queue.get())
self.__flexPtp_log.configure(state="disabled")
while not self.__linuxptp_log_queue.empty():
self.__linuxptp_log.configure(state="normal")
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)
def __init__(self) -> None:
self.__win = ThemedTk(theme="arc")
#self.__win.configure()
self.__win.title("flexPTP test suite")
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.configure(width=1000, height=600)
self.__init_tabs()
self.__tabs.pack(expand=True, fill="both")
self.__init_print_polls()
def mainloop(self) -> None:
self.__win.mainloop()
def __gather_tests(self) -> None:
# gather test cases
self.__tests = []
for name, case in self.__test_cases.items():
ptype = case["type"]
template = {}
if ptype == "general":
template = { "type": ptype, "name": case["delmech"] + " " + case["layer"], "delmech": case["delmech"], "layer": case["layer"] }
elif ptype == "defined":
template = { "type": ptype, "name": name }
if case["master"].get():
master_mode = dict(template)
master_mode["mode"] = "master"
master_mode["name"] += " (master)"
self.__tests.append(master_mode)
if case["slave"].get():
slave_mode = dict(template)
slave_mode["mode"] = "slave"
slave_mode["name"] += " (slave)"
self.__tests.append(slave_mode)
def __populate_tests_treeview(self) -> None:
tw = self.__test_tw
items = tw.get_children()
if items != ():
tw.delete(*items)
seqnum = 1
n = len(self.__tests)
for test in self.__tests:
test["entry"] = tw.insert("", tk.END, text=str(seqnum) + "/" + str(n), values=(test["name"], "?", "Pending", "-", "-"), tags=("pending"))
seqnum += 1
def __open_flexPtp_interface(self) -> None:
url = self.__sel_device.get()
opts = {
"baudrate": self.__sel_baudrate.get(),
"parity": self.__sel_parity.get(),
"stopbits": self.__sel_stopbits.get()
}
def echo(data: str) -> None:
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)
def __init_linuxptp_observer(self) -> None:
self.__linuxptp_observer = LinuxPtpObserver(self.__linuxptp_interface.get())
def echo(data) -> None:
self.__linuxptp_log_queue.put(str(data))
self.__linuxptp_observer.register_observer_callback(echo)
def __init_flexPtp(self) -> None:
ctrl = self.__flexPtp_controller
ctrl.disable_all_logging()
ctrl.set_domain(0)
ctrl.reset_flexptp()
def start_tests(self) -> None:
self.__gather_tests()
self.__populate_tests_treeview()
self.__open_flexPtp_interface()
self.__init_flexPtp()
self.__init_linuxptp_observer()
# ----
ctrl = self.__flexPtp_controller
observer = self.__linuxptp_observer
for test in self.__tests:
if test["type"] == "general":
id = test["delmech"] + "_" + test["layer"]
else:
id = test["name"]
ctrl.start_by_id(id)
priority1 = 128
if test["mode"] == "master":
priority1 = 100
ctrl.set_priority(priority1, 255)
observer.start_linuxptp(id)
for test in self.__tests:
pass
def stop_tests(self) -> None:
self.__linuxptp_observer.stop_linuxptp()
self.__device_interface.close()

View File

@ -1,4 +0,0 @@
class LinuxPtpObserver:
def __init__(self) -> None:
pass

51
LinuxPtpObserver.py Normal file
View File

@ -0,0 +1,51 @@
import signal
import subprocess
import select
from threading import Thread
from typing import Callable
class LinuxPtpObserver:
def __init__(self, ni: str) -> None:
self.__ni = ni
self.__process: subprocess.Popen
self.__observer_thread: Thread
self.__observer_cb: Callable | None = None
pass
def observer_routine(self) -> None:
if self.__process.stdout is not None:
pfd = select.poll()
pfd.register(self.__process.stdout.fileno(), select.POLLIN)
else:
return
while self.__process.poll() is None:
res = pfd.poll()
if len(res) > 0 and res[0][1] & select.POLLIN:
data = self.__process.stdout.readline()
if self.__observer_cb is not None:
self.__observer_cb(data)
def start_linuxptp(self, profile: str) -> None:
cmd = ["sudo", "ptp4l", "--priority1=127", "--priority2=255", "--gmCapable=1", "--neighborPropDelayThresh=100000",
"--min_neighbor_prop_delay=-20000000", "--assume_two_step=1", "--ptp_minor_version=0",
"-i", self.__ni, "-f", "linuxptp_configs/{:s}.cfg".format(profile), "-m", "-l", "6"]
self.__process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
text=True
)
self.__observer_thread = Thread(target=self.observer_routine)
self.__observer_thread.start()
def stop_linuxptp(self) -> None:
self.__process.send_signal(signal.SIGKILL)
self.__observer_thread.join()
def register_observer_callback(self, cb : Callable) -> None:
self.__observer_cb = cb

View File

@ -1,45 +1,40 @@
from DeviceInterface import DeviceInterface from DeviceInterface import DeviceInterface
from LinuxPtpController import LinuxPtpObserver from LinuxPtpObserver import LinuxPtpObserver
from FlexPtpController import FlexPtpController
class TestController: class TestController:
def __reset_flexptp(self) -> None: def __prepare_device(self) -> None:
self.__di.execute_command("ptp reset") self.__fptp_controller.disable_all_logging()
self.__fptp_controller.set_priority(128, 255)
def __start_e2e_l4(self) -> None: self.__fptp_controller.set_domain(0)
self.__di.execute_command("ptp profile preset default") self.__fptp_controller.reset_flexptp()
def __start_p2p_l4(self) -> None:
self.__di.execute_command("ptp profile preset defp2p")
def __start_e2e_l2(self) -> None:
self.__di.execute_command("ptp profile preset default")
self.__di.execute_command("ptp transport 802.3")
self.__reset_flexptp()
def __start_p2p_l2(self) -> None:
self.__di.execute_command("ptp profile preset defp2p")
self.__di.execute_command("ptp transport 802.3")
self.__reset_flexptp()
def __start_gPTP(self) -> None:
self.__di.execute_command("ptp profile preset gPTP")
def __set_priority(self, priority1: int, priority2: int) -> None:
self.__di.execute_command("ptp priority {:d} {:d}".format(priority1, priority2))
def __set_domain(self, domain: int) -> None:
self.__di.execute_command("ptp domain {:d}".format(domain))
def __disable_all_logging(self) -> None:
logging_types = [ "def", "corr", "ts", "info", "locked", "bmca" ]
for lt in logging_types:
self.__di.execute_command("ptp log " + lt + " off", expect_results=False)
def __set_servo_offset(self, offset: int) -> None:
self.__di.execute_command("ptp servo offset {:d}".format(offset))
def __init__(self, di: DeviceInterface, lptp_observer: LinuxPtpObserver) -> None: def __init__(self, di: DeviceInterface, lptp_observer: LinuxPtpObserver) -> None:
self.__di = di self.__di = di
self.__fptp_controller = FlexPtpController(di)
self.__lptp_observer = lptp_observer self.__lptp_observer = lptp_observer
pass pass
def start_test_e2e_udp(self) -> None:
self.__fptp_controller.start_e2e_udp()
self.__lptp_observer.start_linuxptp("E2E_UDP")
def start_test_e2e_l2(self) -> None:
self.__fptp_controller.start_e2e_l2()
self.__lptp_observer.start_linuxptp("E2E_L2")
def start_test_p2p_udp(self) -> None:
self.__fptp_controller.start_p2p_udp()
self.__lptp_observer.start_linuxptp("P2P_UDP")
def start_test_p2p_l2(self) -> None:
self.__fptp_controller.start_p2p_l2()
self.__lptp_observer.start_linuxptp("P2P_L2")
def start_test_gPTP(self) -> None:
self.__fptp_controller.start_gPTP()
self.__lptp_observer.start_linuxptp("gPTP")
def stop_test(self) -> None:
self.__lptp_observer.stop_linuxptp()

BIN
media/flexPTP_test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

122
media/flexPTP_test.svg Normal file
View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="30.975mm"
height="30.975mm"
viewBox="0 0 30.974999 30.975"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
sodipodi:docname="flexPTP_test.svg"
inkscape:export-filename="flexPTP_test.png"
inkscape:export-xdpi="104.96207"
inkscape:export-ydpi="104.96207"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="2.8284271"
inkscape:cx="66.468038"
inkscape:cy="70.357125"
inkscape:window-width="1920"
inkscape:window-height="1172"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g6">
<inkscape:grid
id="grid6"
units="mm"
originx="-68.612541"
originy="-78.809494"
spacingx="0.99999998"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
dotted="false"
gridanglex="30"
gridanglez="30"
visible="false" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Réteg 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-68.612536,-78.809498)">
<g
id="g6"
style="fill:#004455"
transform="matrix(9.6062465,0,0,9.6062465,-590.49611,-678.25486)"
inkscape:export-filename="flexPTP_logo.png"
inkscape:export-xdpi="168.45497"
inkscape:export-ydpi="168.45497">
<text
xml:space="preserve"
style="font-size:2px;line-height:0.75;font-family:'Roboto Condensed';-inkscape-font-specification:'Roboto Condensed, ';letter-spacing:0px;fill:#004455;stroke-width:0.499999;stroke-linecap:square;stroke-miterlimit:4.1;paint-order:stroke fill markers"
x="68.626129"
y="81.948547"
id="text1"><tspan
sodipodi:role="line"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Latin Modern Mono';-inkscape-font-specification:'Latin Modern Mono';fill:#004455;stroke-width:0.5"
x="68.626129"
y="81.948547"
id="tspan2">PTP</tspan></text>
<path
d="m 70.907758,79.254163 c -0.03,0 -0.08198,2.6e-5 -0.08198,0.06203 0,0.06 0.05398,0.05998 0.08198,0.05998 h 0.07999 l 0.235997,0.295977 -0.247993,0.322015 h -0.07999 c -0.03,0 -0.08198,2.5e-5 -0.08198,0.06202 0,0.06 0.05398,0.05998 0.08198,0.05998 h 0.234006 c 0.03,0 0.07999,1.9e-5 0.07999,-0.05998 0,-0.062 -0.04198,-0.06202 -0.103985,-0.06202 l 0.171981,-0.246003 0.178007,0.246003 c -0.056,0 -0.100005,2.5e-5 -0.100005,0.06202 0,0.06 0.04999,0.05998 0.07999,0.05998 h 0.234007 c 0.028,0 0.08198,1.9e-5 0.08198,-0.05998 0,-0.062 -0.05198,-0.06202 -0.08198,-0.06202 h -0.07999 l -0.256009,-0.322015 0.227982,-0.295977 h 0.07999 c 0.028,0 0.08204,1.9e-5 0.08204,-0.05998 0,-0.062 -0.05204,-0.06203 -0.08204,-0.06203 h -0.233953 c -0.03,0 -0.08005,-1.9e-5 -0.08005,0.05998 0,0.062 0.04401,0.06203 0.09801,0.06203 l -0.147989,0.201998 -0.152023,-0.201998 c 0.052,0 0.09602,-2.6e-5 0.09602,-0.06203 0,-0.06 -0.04999,-0.05998 -0.07999,-0.05998 z"
style="-inkscape-font-specification:'Latin Modern Mono';fill:#009f66;fill-opacity:1;stroke-width:0.5;stroke-linecap:square;stroke-miterlimit:4.1;paint-order:stroke fill markers"
id="path9" />
<path
d="m 70.268194,78.892179 c -0.238,0 -0.429981,0.200012 -0.429981,0.446012 0,0.244 0.201998,0.445957 0.459998,0.445957 0.264,0 0.358003,-0.179972 0.358003,-0.229972 0,-0.056 -0.05799,-0.056 -0.06999,-0.056 -0.036,0 -0.05201,0.006 -0.06601,0.044 -0.044,0.102 -0.152023,0.119962 -0.208023,0.119962 -0.15,0 -0.299967,-0.09999 -0.331967,-0.271986 h 0.595991 c 0.042,0 0.07999,3.3e-5 0.07999,-0.07397 0,-0.228 -0.12802,-0.42401 -0.38802,-0.42401 z m 0,0.122007 c 0.104,0 0.228002,0.04995 0.246002,0.255954 h -0.531975 c 0.028,-0.146 0.145973,-0.255954 0.285973,-0.255954 z"
style="-inkscape-font-specification:'Latin Modern Mono';stroke-width:0.5;stroke-linecap:square;stroke-miterlimit:4.1;paint-order:stroke fill markers"
id="path7"
sodipodi:nodetypes="csssscscssccsccs" />
<path
d="m 69.278534,79.256154 c 0.032,0 0.07999,2.9e-5 0.07999,0.06203 0,0.06 -0.05005,0.05998 -0.08005,0.05998 h 0.257999 V 80.35615 H 69.27848 c -0.032,0 -0.08198,2.5e-5 -0.08198,0.06203 0,0.06 0.05199,0.05998 0.07999,0.05998 h 0.658016 c 0.03,0 0.07999,1.9e-5 0.07999,-0.05998 0,-0.062 -0.04799,-0.06203 -0.07999,-0.06203 h -0.25999 v -1.017955 c 0,-0.062 -0.01199,-0.08204 -0.07999,-0.08204 z"
style="-inkscape-font-specification:'Latin Modern Mono';stroke-width:0.5;stroke-linecap:square;stroke-miterlimit:4.1;paint-order:stroke fill markers"
id="path6" />
<path
d="m 69.27848,79.256154 c -0.032,0 -0.08198,2.5e-5 -0.08198,0.06203 0,0.06 0.05199,0.05998 0.07999,0.05998 h 0.002 c 0.03,0 0.08005,1.9e-5 0.08005,-0.05998 0,-0.062 -0.04805,-0.06203 -0.08005,-0.06203 z"
style="-inkscape-font-specification:'Latin Modern Mono';stroke-width:0.5;stroke-linecap:square;stroke-miterlimit:4.1;paint-order:stroke fill markers"
id="path5" />
<path
d="m 69.210484,78.884164 c -0.16,0 -0.313999,0.09201 -0.313999,0.266014 v 0.105976 h -0.200009 c -0.032,0 -0.08198,2.5e-5 -0.08198,0.06203 0,0.06 0.04999,0.05998 0.07999,0.05998 h 0.201999 v 0.617992 h -0.201999 c -0.03,0 -0.08198,3.5e-5 -0.08198,0.06003 0,0.062 0.05198,0.06197 0.08198,0.06197 h 0.542035 c 0.03,0 0.08198,1.9e-5 0.08198,-0.05998 0,-0.062 -0.05198,-0.06203 -0.08198,-0.06203 h -0.201999 v -0.617992 h 0.241968 c -0.028,0 -0.07999,1.5e-5 -0.07999,-0.05998 0,-0.062 0.04998,-0.06203 0.08198,-0.06203 h -0.243958 v -0.09398 c 0,-0.156 0.133983,-0.156005 0.193983,-0.156005 0,0.008 0.01801,0.08602 0.08801,0.08602 0.04,0 0.08596,-0.03201 0.08596,-0.08801 0,-0.12 -0.159993,-0.120016 -0.191993,-0.120016 z"
style="-inkscape-font-specification:'Latin Modern Mono';stroke-width:0.5;stroke-linecap:square;stroke-miterlimit:4.1;paint-order:stroke fill markers"
id="path4" />
<text
xml:space="preserve"
style="font-size:0.367237px;line-height:1.05;font-family:'Roboto Condensed';-inkscape-font-specification:'Roboto Condensed, ';text-align:justify;letter-spacing:0.00413143px;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:none;fill-opacity:1;stroke:#b8913f;stroke-width:0.0104099;stroke-linecap:square;stroke-miterlimit:4.1;paint-order:markers fill stroke"
x="71.461769"
y="79.758598"
id="text9"><tspan
sodipodi:role="line"
id="tspan9"
style="stroke-width:0.0104099"
x="71.461769"
y="79.758598" /></text>
<rect
style="fill:none;fill-opacity:1;stroke:#ff461a;stroke-width:0.0520495;stroke-linecap:square;stroke-miterlimit:4.1;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect9"
width="1.0707279"
height="1.019085"
x="70.740189"
y="79.169914" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

9
requirements.txt Normal file
View File

@ -0,0 +1,9 @@
future==1.0.0
iso8601==2.1.0
netifaces==0.11.0
netinterfaces==0.1.0
pillow==12.2.0
pyserial==3.5
PyYAML==6.0.3
ttk-text==0.3.3
ttkthemes==3.3.0

View File

@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
COMMON_FLAGS=--priority1=127 --priority2=255 --gmCapable=1 --neighborPropDelayThresh=100000 --min_neighbor_prop_delay=-20000000 --assume_two_step=1 --ptp_minor_version=0 COMMON_FLAGS="--priority1=127 --priority2=255 --gmCapable=1 --neighborPropDelayThresh=100000 --min_neighbor_prop_delay=-20000000 --assume_two_step=1 --ptp_minor_version=0"
ptp4l -i "$1" -f "linuxptp_configs/$2.cfg" -m -l 6 $COMMON_FLAGS sudo ptp4l -i "$1" -f "linuxptp_configs/$2.cfg" -m -l 6 $COMMON_FLAGS

View File

@ -1,12 +1,23 @@
#!/usr/bin/python3 #!/usr/bin/python3
import optparse import optparse
import os
import time
import DeviceInterface import DeviceInterface
from LinuxPtpObserver import LinuxPtpObserver
from TestController import TestController
from GUI import GUI
# ---------------------------------- # ----------------------------------
usage = "<device/url> [-s <baudrate>]" gui = GUI()
gui.mainloop()
exit(0)
usage = "<device/url> <interface> [-s <baudrate>]"
parser = optparse.OptionParser(usage=usage) parser = optparse.OptionParser(usage=usage)
parser.add_option("-s", dest="baudrate", default=115200) parser.add_option("-s", dest="baudrate", default=115200)
@ -23,10 +34,33 @@ di = DeviceInterface.DeviceInterface(url=args[0], options={"baudrate": opts.baud
#results = di.execute_command("osinfo") #results = di.execute_command("osinfo")
#print(results) #print(results)
def echo(str: str) -> None:
print(str, end="")
di.register_out_callback(echo)
# get device clock identity # get device clock identity
OWN_CLOCK_ID_KEY = "Own clock ID" OWN_CLOCK_ID_KEY = "Own clock ID"
clock_id = di.execute_command("ptp info", separate_results=True) clock_id = di.execute_command("ptp info", separate_results=True)
if type(clock_id) == dict[str, str] and OWN_CLOCK_ID_KEY in clock_id: if type(clock_id) == dict and OWN_CLOCK_ID_KEY in clock_id:
print("Own clock ID:", clock_id[OWN_CLOCK_ID_KEY]) print("Own clock ID:", clock_id[OWN_CLOCK_ID_KEY])
else: else:
print("Could not retrieve device clock ID!") print("Could not retrieve device clock ID!")
lptp_observer = LinuxPtpObserver(args[1])
lptp_observer.register_observer_callback(echo)
tc = TestController(di, lptp_observer)
tc.start_test_gPTP()
time.sleep(15)
tc.stop_test()
tc.start_test_e2e_udp()
time.sleep(15)
tc.stop_test()
di.close()