import queue import signal import subprocess import select from threading import Thread import time 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: 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: self.__ni = ni self.__process: subprocess.Popen self.__observer_thread: Thread self.__observer_cb: Callable | None = None self.__event_queue = queue.Queue() self.__error_queue = queue.Queue() pass def observer_routine(self) -> None: pfd = select.poll() if self.__process.stdout is not None: pfd.register(self.__process.stdout.fileno(), select.POLLIN) else: return if self.__process.stderr is not None: pfd.register(self.__process.stderr.fileno(), select.POLLIN) else: return while self.__process.poll() is None: res = pfd.poll() for fde in res: if fde[0] == self.__process.stdout.fileno() and fde[1] & select.POLLIN: # STDOUT orig_data = self.__process.stdout.readline() 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: cmd = ["sudo", "ptp4l", "--priority1=127", "--priority2=255", "--gmCapable=1", "--neighborPropDelayThresh=100000", "--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"] self.__process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=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() self.__event_queue = queue.Queue() def register_observer_callback(self, cb : Callable) -> None: 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