155 lines
5.4 KiB
Python
155 lines
5.4 KiB
Python
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
|
|
|
|
|