#!/usr/bin/env python3 import os import sys import time import datetime import subprocess import json PTP_REMOTE_CONFIG_PATH = "/opt/ptp_conf" SLAVE_FILE = "ptp4l_slave.conf" MASTER_FILE = "ptp4l_master.conf" NODE_ADDRESSES_PATH = "/home/apu/testbed_files/apu-tb-opt/scripts/mesh/node_addresses.sh" PTP4L_INSTANCE = "/home/apu/wifi-ptp/ptp/ptp4l" # Use modified ptp4l SLAVE_LOG = "/tmp/ptp4l_slave.log" MASTER_LOG = "/tmp/ptp4l_master.log" PHC2SYS_LOG = "/tmp/phc2sys.log" LOG_LABELS = { SLAVE_LOG: "ptp4l_slave", MASTER_LOG: "ptp4l_master", PHC2SYS_LOG: "phc2sys" } # Maps node hostname to a list of active roles node_roles = {} # def build_ptp_unicast_slave_config(master_ip: str, interface: str) -> dict: # return { # "global": { # "twoStepFlag": "1", # "priority1": "248", # "priority2": "128", # "slaveOnly": "1", # "domainNumber": "0", # "timeSource": "0xA0", # "network_transport": "UDPv4", # "delay_mechanism": "P2P", # "logSyncInterval": "-3", # "logMinDelayReqInterval": "-3", # "logAnnounceInterval": "1", # "logMinPdelayReqInterval": "-3", # "pi_proportional_const": "0.7", # "pi_integral_const": "0.3" # }, # "unicast_master_table": { # "table_id": "1", # "logQueryInterval": "2", # "peer_address": master_ip, # "UDPv4": master_ip # }, # interface: { # "unicast_master_table": "1" # } # } # def build_ptp_master_config(interface: str) -> dict: # return { # "global": { # "twoStepFlag": "1", # "clockClass": "128", # "clockAccuracy": "0x20", # "offsetScaledLogVariance": "0x200", # "priority1": "128", # "priority2": "128", # "domainNumber": "0", # "timeSource": "0x10", # "network_transport": "UDPv4", # "delay_mechanism": "P2P", # "logSyncInterval": "-3", # "logMinDelayReqInterval": "-3", # "logAnnounceInterval": "1", # "logMinPdelayReqInterval": "-3", # "pi_proportional_const": "0.7", # "pi_integral_const": "0.3" # }, # interface: {} # } # def build_ptp_unicast_slave_config(master_ip: str, interface: str) -> dict: # return { # "global": { # "twoStepFlag": "1", # "priority1": "248", # "priority2": "128", # "slaveOnly": "1", # "domainNumber": "0", # "timeSource": "0xA0", # "network_transport": "UDPv4", # "delay_mechanism": "P2P", # "hybrid_e2e": "0", # "logSyncInterval": "0", # "logMinDelayReqInterval": "0", # "logAnnounceInterval": "1", # "logMinPdelayReqInterval": "0", # "pi_proportional_const": "0.7", # "pi_integral_const": "0.3" # }, # "unicast_master_table": { # "table_id": "1", # "logQueryInterval": "2", # "peer_address": master_ip, # "UDPv4": master_ip # }, # interface: { # "unicast_master_table": "1" # } # } # def build_ptp_master_config(interface: str) -> dict: # return { # "global": { # "twoStepFlag": "1", # "clockClass": "128", # "clockAccuracy": "0x20", # "offsetScaledLogVariance": "0x200", # "priority1": "128", # "priority2": "128", # "domainNumber": "0", # "timeSource": "0x10", # "network_transport": "UDPv4", # "delay_mechanism": "P2P", # "hybrid_e2e": "0", # "logSyncInterval": "0", # "logMinDelayReqInterval": "0", # "logAnnounceInterval": "1", # "logMinPdelayReqInterval": "0", # "pi_proportional_const": "0.7", # "pi_integral_const": "0.3" # }, # interface: {} # } # def build_ptp_unicast_slave_config(master_ip: str, interface: str, filter:str) -> dict: # return { # "global": { # "filter_id": filter, # "slaveOnly": "1", # "network_transport": "UDPv4", # "delay_mechanism": "P2P", # "hybrid_e2e": "0" # }, # "unicast_master_table": { # "table_id": "1", # "peer_address": master_ip, # "UDPv4": master_ip # }, # interface: { # "unicast_master_table": "1" # } # } # def build_ptp_master_config(interface: str) -> dict: # return { # "global": { # # "masterOnly": "1", # "network_transport": "UDPv4", # "delay_mechanism": "P2P", # "hybrid_e2e": "0" # }, # interface: {} # } def build_ptp_unicast_slave_config(master_ip: str, interface: str, filter:str) -> dict: return { "global": { "filter_id": filter, "slaveOnly": "1", "network_transport": "UDPv4", "delay_mechanism": "E2E", "hybrid_e2e": "0" }, interface: {} } def build_ptp_master_config(interface: str) -> dict: return { "global": { "masterOnly": "1", "network_transport": "UDPv4", "delay_mechanism": "E2E", "hybrid_e2e": "0" }, interface: {} } # # chat gpt mesh config: # def build_ptp_master_config(interface: str) -> dict: # return { # "global": { # "uds_address": "/var/run/ptp4l", # "twoStepFlag": "1", # "clockClass": "128", # "clockAccuracy": "0x20", # "offsetScaledLogVariance": "0x200", # "priority1": "128", # "priority2": "128", # "domainNumber": "0", # "timeSource": "0x10", # "network_transport": "UDPv4", # "delay_mechanism": "P2P", # "hybrid_e2e": "0", # "logSyncInterval": "0", # "logMinDelayReqInterval": "0", # "logAnnounceInterval": "1", # "logMinPdelayReqInterval": "0", # "pi_proportional_const": "0.7", # "pi_integral_const": "0.3" # }, # interface: {} # } # def build_ptp_unicast_slave_config(master_ip: str, interface: str, filter:str) -> dict: # return { # "global": { # "filter_id": filter, # "uds_address": "/var/run/ptp4l", # "twoStepFlag": "1", # "priority1": "248", # "priority2": "128", # "slaveOnly": "1", # "domainNumber": "0", # "timeSource": "0xA0", # "network_transport": "UDPv4", # "delay_mechanism": "P2P", # "hybrid_e2e": "0", # "logSyncInterval": "0", # "logMinDelayReqInterval": "0", # "logAnnounceInterval": "1", # "logMinPdelayReqInterval": "0", # "pi_proportional_const": "0.7", # "pi_integral_const": "0.3" # }, # "unicast_master_table": { # "table_id": "1", # "logQueryInterval": "2", # "peer_address": master_ip, # "UDPv4": master_ip # }, # interface: { # "unicast_master_table": "1" # } # } def write_remote_config(host: str, content: str, filename: str): subprocess.run([ "ssh", host, f"echo '{content}' | sudo tee {PTP_REMOTE_CONFIG_PATH}/{filename} > /dev/null" ], check=True) def run_remote_command(host: str, cmd: str): subprocess.run(["ssh", host, cmd], check=True) def config_to_string(config: dict) -> str: lines = [] for section, options in config.items(): if isinstance(options, dict): lines.append(f"[{section}]") for k, v in options.items(): lines.append(f"{k} {v}") else: lines.append(f"[{section}]\n{options}") return "\n".join(lines) def stop_all_ptp(host): run_remote_command(host, "pkill -x ptp4l || true") run_remote_command(host, "pkill -x phc2sys || true") run_remote_command(host, "sync") def fetch_logs(hostname: str, output_file: object): expected_logs = node_roles.get(hostname, []) for log_path, label in LOG_LABELS.items(): if label not in expected_logs: print(f"[DEBUG] Skipping log '{label}' on {hostname}") continue try: result = subprocess.run( ["ssh", hostname, f"sync; sleep 1; cat {log_path}"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) cleaned = result.stdout.replace('\x00', '') output_file.write(f"===== BEGIN {label.upper()} LOG FROM {hostname} =====\n") output_file.write(cleaned) output_file.write(f"\n===== END {label.upper()} LOG FROM {hostname} =====\n\n") except subprocess.CalledProcessError as e: output_file.write(f"===== ERROR FETCHING {label.upper()} LOG FROM {hostname} =====\n") output_file.write(e.stderr + "\n") def load_mac_suffix_from_bash_array(filepath, array_name, node_number): hostname = f"apu{int(node_number):02d}" mac = None with open(filepath, "r") as file: lines = file.readlines() in_array = False for line in lines: stripped = line.strip() if stripped.startswith(f"declare -A {array_name}"): in_array = True continue if in_array: if stripped.startswith(")"): break # end of array if stripped.startswith(f'["{hostname}"]'): mac = stripped.split("=")[-1].strip().strip('"') break if mac: last_three = ''.join(mac.lower().split(':')[-3:]) return last_three else: raise ValueError(f"No MAC found for node {hostname} in array {array_name}") def start_ptp_roles(node_id, node_list, iface): hostname = f"apu{node_id:02d}" # iface = "adhoc0" phc = "/dev/ptp3" node_roles[hostname] = [] idx = node_list.index(node_id) is_first = idx == 0 is_last = idx == len(node_list) - 1 stop_all_ptp(hostname) if not is_first: master_ip = f"192.168.10.{10 + node_list[idx - 1]}" filter_id = load_mac_suffix_from_bash_array(NODE_ADDRESSES_PATH, "phy0MAC", node_list[idx - 1]) print(f"[On node {hostname}] {filter_id} is master") slave_conf = build_ptp_unicast_slave_config(master_ip, iface, filter_id) slave_str = config_to_string(slave_conf) write_remote_config(hostname, slave_str, SLAVE_FILE) run_remote_command(hostname, f"{PTP4L_INSTANCE} -i {iface} -m -H -p {phc} -f {PTP_REMOTE_CONFIG_PATH}/{SLAVE_FILE} > {SLAVE_LOG} 2>&1 &") # run_remote_command(hostname, f"{PTP4L_INSTANCE} -i {iface} -m -S -p {phc} > {SLAVE_LOG} 2>&1 &") # without config # run_remote_command(hostname, f"phc2sys -s {phc} -c CLOCK_REALTIME -O 0 -m > {PHC2SYS_LOG} 2>&1 &") run_remote_command(hostname, f"phc2sys -s {phc} -c CLOCK_REALTIME -O 0 -m > {PHC2SYS_LOG} 2>&1 &") # without offset threshold set to 0 node_roles[hostname].append(LOG_LABELS[SLAVE_LOG]) node_roles[hostname].append(LOG_LABELS[PHC2SYS_LOG]) if not is_last: master_conf = build_ptp_master_config(iface) master_str = config_to_string(master_conf) write_remote_config(hostname, master_str, MASTER_FILE) run_remote_command(hostname, f"{PTP4L_INSTANCE} -i {iface} -m -H -p {phc} -f {PTP_REMOTE_CONFIG_PATH}/{MASTER_FILE} > {MASTER_LOG} 2>&1 &") # run_remote_command(hostname, f"{PTP4L_INSTANCE} -i {iface} -m -S -p {phc} > {MASTER_LOG} 2>&1 &") # without config node_roles[hostname].append(LOG_LABELS[MASTER_LOG]) # print(f"[DEBUG] Node {hostname} will produce logs: {node_roles[hostname]}") def main(): if len(sys.argv) < 3: print("Usage: ./run_ptp4l_ibss.py [ ...]") sys.exit(1) interfaces = ["adhoc0", "mesh0"] print("Select interface to use:") for idx, option in enumerate(interfaces, 1): print(f" {idx}) {option}") while True: try: choice = int(input("Enter choice number: ")) if 1 <= choice <= len(interfaces): iface = interfaces[choice - 1] break else: print("Invalid choice. Try again.") except ValueError: print("Please enter a number.") duration = int(sys.argv[1]) nodes = list(map(int, sys.argv[2:])) print("[INFO] Stopping ntp processes...") for node in nodes: hostname = f"apu{node:02d}" run_remote_command(hostname, "sudo systemctl stop systemd-timesyncd.service") print(f"[INFO] Starting PTP test with nodes {nodes} for {duration}s") for node_id in nodes: start_ptp_roles(node_id, nodes, iface) # print("\n[DEBUG] Node roles assigned:") # for hostname, roles in node_roles.items(): # print(f" {hostname}: {roles}") for node in nodes: hostname = f"apu{node:02d}" if not node_roles.get(hostname): print(f"[WARN] No roles assigned to {hostname}") print("[INFO] Waiting for test duration...") for remaining in range(duration, 0, -1): hrs, secs = divmod(remaining, 3600) mins, secs = divmod(secs, 60) print(f"[WAIT] Remaining: {hrs:02d}:{mins:02d}:{secs:02d}", end="\r") time.sleep(1) print() print("[INFO] Stopping ptp and starting ntp processes...") for node in nodes: hostname = f"apu{node:02d}" stop_all_ptp(hostname) run_remote_command(hostname, "sudo systemctl start systemd-timesyncd.service") timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") log_file = f"ptp_{iface}_{timestamp}_nodes({'-'.join(map(str, nodes))}).log" with open(log_file, "w") as outfile: for node_id in nodes: hostname = f"apu{node_id:02d}" fetch_logs(hostname, outfile) print(f"[DONE] PTP test complete. Logs saved to {log_file}") if __name__ == "__main__": main()