#!/usr/bin/env python3

#
# Copyright © 2021 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
#
# This software product is a proprietary product of Nvidia Corporation and its affiliates
# (the "Company") and all right, title, and interest in and to the software
# product, including all associated intellectual property rights, are and
# shall remain exclusively with the Company.
#
# This software product is governed by the End User License Agreement
# provided with the software product. 
# All Rights reserved.

import socket
import os
import binascii
import struct
import json
import sys
import ipaddress
from datetime import datetime
from datetime import timezone

ptp_sequence_id = 1

ptp4lsys_port_map = {}

class Ptp4lConstants:
    server_address = '/var/run/ptp4l'
    client_address = '/var/run/ptp4l.client'

    DDS_TWO_STEP_FLAG = (1 << 0)
    DDS_SLAVE_ONLY = (1 << 1)

    LEAP_61 = (1 << 0)
    LEAP_59 = (1 << 1)
    UTC_OFF_VALID = (1 << 2)
    PTP_TIMESCALE = (1 << 3)
    TIME_TRACEABLE = (1 << 4)
    FREQ_TRACEABLE = (1 << 5)

    PTP_SOURCE_PORT_ID = 'CC:CC:CC:CC:00:00:EE:EE:EE:EE'
    PTP_ALL_PORT_ID = 'FF:FF:FF:FF:FF:FF:FF:FF:FF:FF'
    PTP_DEFAULT_CLOCK_ID = 'FF:FF:FF:FF:FF:FF:FF:FF'
    PTP_DEFAULT_BNDRY_HOPS = 1

    PTP_VERSION = 2

    PTP_MSG_SYNC = 0x0
    PTP_MSG_DELAY_REQ = 0x1
    PTP_MSG_PDELAY_REQ = 0x2
    PTP_MSG_PDELAY_RESP = 0x3
    PTP_MSG_FOLLOW_UP = 0x8
    PTP_MSG_DELAY_RESP = 0x9
    PTP_MSG_PDELAY_RESP_FOLLOW_UP = 0xA
    PTP_MSG_ANNOUNCE = 0xB
    PTP_MSG_SIGNALING = 0xC
    PTP_MSG_MANAGEMENT = 0xD

    PTP_TGT_PORT_ALL = 0xFFFF

    # PTP Management Commands
    PTP_MANAGEMENT_ACTION_GET = 0
    PTP_MANAGEMENT_ACTION_SET = 1
    PTP_MANAGEMENT_ACTION_RESPONSE = 2
    PTP_MANAGEMENT_ACTION_COMMAND = 3
    PTP_MANAGEMENT_ACTION_ACKNOWLEDGE = 4

    # PTP message types
    PTP_MSG_CTL_SYNC = 0
    PTP_MSG_CTL_DELAY_REQ = 1
    PTP_MSG_CTL_FOLLOW_UP = 2
    PTP_MSG_CTL_DELAY_RESP = 3
    PTP_MSG_CTL_MANAGEMENT = 4
    PTP_MSG_CTL_OTHER = 5

    # TLV Types
    TLV_MANAGEMENT = 0x0001
    TLV_MANAGEMENT_ERROR_STATUS = 0x0002

    # Clock Management ID values
    TLV_USER_DESCRIPTION = 0x0002
    TLV_SAVE_IN_NON_VOLATILE_STORAGE = 0x0003
    TLV_RESET_NON_VOLATILE_STORAGE = 0x0004
    TLV_INITIALIZE = 0x0005
    TLV_FAULT_LOG = 0x0006
    TLV_FAULT_LOG_RESET = 0x0007
    TLV_DEFAULT_DATA_SET = 0x2000
    TLV_CURRENT_DATA_SET = 0x2001
    TLV_PARENT_DATA_SET = 0x2002
    TLV_TIME_PROPERTIES_DATA_SET = 0x2003
    TLV_PRIORITY1 = 0x2005
    TLV_PRIORITY2 = 0x2006
    TLV_DOMAIN = 0x2007
    TLV_SLAVE_ONLY = 0x2008
    TLV_TIME = 0x200F
    TLV_CLOCK_ACCURACY = 0x2010
    TLV_UTC_PROPERTIES = 0x2011
    TLV_TRACEABILITY_PROPERTIES = 0x2012
    TLV_TIMESCALE_PROPERTIES = 0x2013
    TLV_PATH_TRACE_LIST = 0x2015
    TLV_PATH_TRACE_ENABLE = 0x2016
    TLV_GRANDMASTER_CLUSTER_TABLE = 0x2017
    TLV_ACCEPTABLE_MASTER_TABLE = 0x201A
    TLV_ACCEPTABLE_MASTER_MAX_TABLE_SIZE = 0x201C
    TLV_ALTERNATE_TIME_OFFSET_ENABLE = 0x201E
    TLV_ALTERNATE_TIME_OFFSET_NAME = 0x201F
    TLV_ALTERNATE_TIME_OFFSET_MAX_KEY = 0x2020
    TLV_ALTERNATE_TIME_OFFSET_PROPERTIES = 0x2021
    TLV_EXTERNAL_PORT_CONFIGURATION_ENABLED = 0x3000
    TLV_HOLDOVER_UPGRADE_ENABLE = 0x3002
    TLV_TRANSPARENT_CLOCK_DEFAULT_DATA_SET = 0x4000
    TLV_PRIMARY_DOMAIN = 0x4002
    TLV_TIME_STATUS_NP = 0xC000
    TLV_GRANDMASTER_SETTINGS_NP = 0xC001
    TLV_SUBSCRIBE_EVENTS_NP = 0xC003
    TLV_SYNCHRONIZATION_UNCERTAIN_NP = 0xC006
    TLV_PORT_COUNTERS = 0xC008
    TLV_FOREIGN_MASTERS_DATA_SET = 0xC009

    TLV_ACCEPTABLE_MASTER_LOG_NP = 0xD001
    TLV_FORCED_MASTER_LOG_NP = 0xD002
    TLV_LOGGING_ENABLE_NP = 0xD003
    TLV_LOGGING_DISABLE_NP = 0xD004
    TLV_CLEAR_ACCEPTABLE_MASTER_LOG_NP = 0xD005
    TLV_CLEAR_FORCED_MASTER_LOG_NP = 0xD006
    TLV_OFFSET_MIN_THRESHOLD_NP = 0xD007
    TLV_OFFSET_MAX_THRESHOLD_NP = 0xD008
    TLV_MPD_THRESHOLD_NP = 0xD009

    TLV_NULL_MANAGEMENT = 0x0000
    TLV_CLOCK_DESCRIPTION = 0x0001
    TLV_PORT_DATA_SET = 0x2004
    TLV_LOG_ANNOUNCE_INTERVAL = 0x2009
    TLV_ANNOUNCE_RECEIPT_TIMEOUT = 0x200A
    TLV_LOG_SYNC_INTERVAL = 0x200B
    TLV_VERSION_NUMBER = 0x200C
    TLV_ENABLE_PORT = 0x200D
    TLV_DISABLE_PORT = 0x200E
    TLV_UNICAST_NEGOTIATION_ENABLE = 0x2014
    TLV_UNICAST_MASTER_TABLE = 0x2018
    TLV_UNICAST_MASTER_MAX_TABLE_SIZE = 0x2019
    TLV_ACCEPTABLE_MASTER_TABLE_ENABLED = 0x201B
    TLV_ALTERNATE_MASTER = 0x201D
    TLV_MASTER_ONLY = 0x3001
    TLV_EXT_PORT_CONFIG_PORT_DATA_SET = 0x3003
    TLV_SLAVE_EVENT_MONITORING = 0x3004

    TLV_TRANSPARENT_CLOCK_PORT_DATA_SET = 0x4001
    TLV_DELAY_MECHANISM = 0x6000
    TLV_LOG_MIN_PDELAY_REQ_INTERVAL = 0x6001
    TLV_PORT_DATA_SET_NP = 0xC002
    TLV_PORT_PROPERTIES_NP = 0xC004
    TLV_PORT_STATS_NP = 0xC005
    TLV_FOREIGN_MASTERS_DATA_SET = 0xC008


ptp4lconst = Ptp4lConstants()


def ptp_build_portid(clockid, port):
    port_id = '{0:04x}'.format(port)
    port_id = clockid + ':' +  \
        ':'.join(port_id[i:i + 2] for i in range(0, 4, 2))
    return port_id


# PTP Messages Header
class PTPHeader:
    def __init__(self,
                 tsmt=0,
                 ver=0,
                 msg_len=0,
                 domain=0,
                 source_port_id='00:00:00:00:00:00:00:00:00:00',
                 seq=0,
                 control=0,
                 log_msg_interval=0):
        self.tsmt = tsmt
        self.version = ver
        self.messageLength = msg_len
        self.domain_number = domain
        self.source_port_id = source_port_id
        self.sequence_id = seq
        self.control = control
        self.log_msg_interval = log_msg_interval

    def build(self):
        port_id = binascii.unhexlify(self.source_port_id.replace(':', ''))
        hdr = struct.pack('!BBHBBHQL10sHBB',
                          self.tsmt & 0x0F,
                          self.version & 0x0F,
                          self.messageLength,
                          self.domain_number,
                          0,
                          0,
                          0,
                          0,
                          port_id,
                          self.sequence_id,
                          self.control,
                          self.log_msg_interval,)

        return hdr

    def size(self):
        return struct.calcsize('!BBHBBHQL10sHBB')

    def hdr_unpack(self, data):
        return struct.unpack('>BBHBBHQL10sHBB')

    def __str__(self):
        return str(self.__class__) + ": " + str(self.__dict__)


# PTP Management messages - follows the PTP Messages Header
class PTPManagementMsg:
    def __init__(self,
                 target_port_id='00:00:00:00:00:00:00:00:00:00',
                 starting_boundary_hops=0,
                 boundary_hops=0,
                 action=0):
        self.target_port_id = target_port_id
        self.starting_boundary_hops = starting_boundary_hops
        self.boundary_hops = boundary_hops
        self.action = action

    def build(self):
        port_id = binascii.unhexlify(self.target_port_id.replace(':', ''))
        mgmt_msg = struct.pack('!10sBBBB',
                               port_id,
                               self.starting_boundary_hops,
                               self.boundary_hops,
                               self.action, 0)
        return mgmt_msg

    def size(self):
        return struct.calcsize('!10sBBBB')

    def __str__(self):
        return str(self.__class__) + ": " + str(self.__dict__)


# TLV portion of the PTP Management Message
class PTPManagementTLV:
    def __init__(self,
                 tlv_type=0,
                 tlv_length=0,
                 tlv_id=0,):
        self.type = tlv_type
        self.length = tlv_length
        self.mgmt_tlv_id = tlv_id

    def build(self):
        mgmt_tlv = struct.pack('!HHH',
                               self.type,
                               self.length,
                               self.mgmt_tlv_id,)
        return mgmt_tlv

    def size(self):
        return struct.calcsize('!HHH')


class PTPdefaultDS:
    def __init__(self,
                 flags=0,
                 number_of_ports=0,
                 priority1=0,
                 clock_class=0,
                 clock_accuracy=0,
                 offset_sclaled_log_variance=0,
                 priority2=0,
                 clock_id='00:00:00:00:00:00:00:00',
                 domain_number=0,):
        self.flags = flags
        self.numberOfPorts = number_of_ports
        self.priority1 = priority1
        self.clockClass = clock_class
        self.clockAccuracy = clock_accuracy
        self.offsetSclaledLogVariance = offset_sclaled_log_variance
        self.priority2 = priority2
        self.clockId = clock_id
        self.domainNumber = domain_number

    def build(self):
        clock_id = binascii.unhexlify(self.clockId.replace(':', ''))
        default_ds = struct.pack('!BBHBBBHB8sBB',
                                 self.flags, 0,
                                 self.numberOfPorts,
                                 self.priority1,
                                 self.clockClass,
                                 self.clockAccuracy,
                                 self.offsetSclaledLogVariance,
                                 self.priority2,
                                 clock_id,
                                 self.domainNumber, 0, )
        return default_ds

    def size(self):
        return struct.calcsize('!BBHBBBHB8sBB')

    def build_dict(self, data, offset):
        ds = {}
        clockquality = {}
        if (len(data) - offset) < (self.size()):
            return ds
        def_tup = struct.unpack_from('>BBHBBBHB8sBB', data, offset)
        ds['two-step'] = 'on' \
            if (def_tup[0] & ptp4lconst.DDS_TWO_STEP_FLAG) else 'off'
        ds['slave-only'] = 'on' \
            if (def_tup[0] & ptp4lconst.DDS_SLAVE_ONLY) else 'off'
        ds['number-ports'] = def_tup[2]
        ds['priority1'] = def_tup[3]
        clockquality['clock-class'] = def_tup[4]
        clockquality['clock-accuracy'] = def_tup[5]
        clockquality['offset-sclaled-log-variance'] = def_tup[6]
        ds['clock-quality'] = clockquality

        ds['priority2'] = def_tup[7]

        off = offset + 10
        clkid_tup = struct.unpack_from('>BBBBBBBB', data, off)
        clkid = ''
        for x in clkid_tup:
            clkid += '{0:02x}'.format(x)
        clkid_str = ':'.join(clkid[i:i + 2] for i in range(0, 16, 2))
        ds['clock-id'] = clkid_str

        off += 8
        dtup = struct.unpack_from('>BB', data, off)
        ds['domain'] = dtup[0]
        return ds

    def __str__(self):
        return str(self.__class__) + ": " + str(self.__dict__)


class PTPparentDS:
    def __init__(self,
                 parent_port_id='00:00:00:00:00:00:00:00:00:00',
                 parent_stats=0,
                 obs_parent_off_log_var=0,
                 obs_arent_phase_change_rate=0,
                 gm_priority1=0,
                 gm_clock_class=0,
                 gm_clock_accuracy=0,
                 gm_clk_off_log_variance=0,
                 gm_priority2=0,
                 gm_clock_id='00:00:00:00:00:00:00:00'):
        self.parentPortId = parent_port_id
        self.parentStats = parent_stats
        self.observedParentOffsetScaledLogVariance = obs_parent_off_log_var
        self.observedParentClockPhaseChangeRate = obs_arent_phase_change_rate
        self.grandmasterPriority1 = gm_priority1
        self.grandmasterClockClass = gm_clock_class
        self.grandmasterClockAccuracy = gm_clock_accuracy
        self.grandmasterClockoffsetScaledLogVariance = gm_clk_off_log_variance
        self.grandmasterPriority2 = gm_priority2
        self.grandmasterClockId = gm_clock_id

    def build(self):
        pp_id = binascii.unhexlify(self.parentPortId.replace(':', ''))
        clock_id = binascii.unhexlify(self.grandmasterClockId.replace(':', ''))
        parent_ds = struct.pack('!10sBBHIBBBHB8s',
                                pp_id,
                                self.parentStats, 0,
                                self.observedParentOffsetScaledLogVariance,
                                self.observedParentClockPhaseChangeRate,
                                self.grandmasterPriority1,
                                self.grandmasterClockClass,
                                self.grandmasterClockAccuracy,
                                self.grandmasterClockoffsetScaledLogVariance,
                                self.grandmasterPriority2,
                                clock_id,)
        return parent_ds

    def size(self):
        return struct.calcsize('!10sBBHIBBBHB8s')

    def build_dict(self, data, offset):
        ds = {}
        clockquality = {}
        par_port_id = ''
        gm_id = ''

        if (len(data) - offset) < (self.size()):
            return ds
        # Unpack the Parent port ID
        par_port_tup = struct.unpack_from('>BBBBBBBBBB', data, offset)
        for x in par_port_tup:
            par_port_id += '{0:02x}'.format(x)
        clkid_str = ':'.join(par_port_id[i:i + 2] for i in range(0, 16, 2))
        pid_str = ':'.join(par_port_id[i:i + 2] for i in range(16, 20, 2))
        portid_str = clkid_str + '-' + pid_str

        off = offset + 10

        # Unpack remaining except GM ID
        par_tup = struct.unpack_from('>BBHIBBBHB8s', data, off)
        ds['parent-port-id'] = portid_str
        ds['parent-stats'] = 'on' if (par_tup[0] == 1) else 'off'
        ds['obs-parent-offset-scaled-log-variance'] = par_tup[2]
        ds['obs-parent-clock-phase-change-rate'] = par_tup[3]
        ds['grandmaster-priority1'] = par_tup[4]

        clockquality['clock-class'] = par_tup[5]
        clockquality['clock-accuracy'] = par_tup[6]
        clockquality['offset-scaled-log-variance'] = par_tup[7]
        ds['grandmaster-clock-quality'] = clockquality

        ds['grandmaster-priority2'] = par_tup[8]

        # Unpack the GM ID
        off = off + 14
        gm_id_tup = struct.unpack_from('>BBBBBBBB', data, off)
        for x in gm_id_tup:
            gm_id += '{0:02x}'.format(x)
        clkid_str = ':'.join(gm_id[i:i + 2] for i in range(0, 16, 2))
        ds['grandmaster-id'] = clkid_str

        return ds


class PTPcurrentDS:
    def __init__(self,
                 steps_removed=0,
                 offset_from_master=0,
                 mean_path_delay=0,):
        self.stepsRemoved = steps_removed
        self.offsetFromMaster = offset_from_master
        self.meanPathDelay = mean_path_delay

    def build(self):
        current_ds = struct.pack('!Hqq',
                                 self.stepsRemoved,
                                 self.offsetFromMaster,
                                 self.meanPathDelay,)
        return current_ds

    def size(self):
        return struct.calcsize('!Hqq')

    def build_dict(self, data, offset):
        ds = {}
        if (len(data) - offset) < (self.size()):
            return ds
        cur_tup = struct.unpack_from('>Hqq', data, offset)
        ds['steps-removed'] = cur_tup[0]
        ds['offset-from-master'] = cur_tup[1]
        ds['mean-path-delay'] = cur_tup[2]
        return ds


class PTPtimePropertiesDS:
    def __init__(self,
                 current_utc_offset=0,
                 flags=0,
                 time_source=0,):
        self.currentUtcOffset = current_utc_offset
        self.flags = flags
        self.timeSource = time_source

    def build(self):
        time_properties_ds = struct.pack('!HBB',
                                         self.currentUtcOffset,
                                         self.flags,
                                         self.timeSource,)
        return time_properties_ds

    def size(self):
        return struct.calcsize('!HBB')

    def build_dict(self, data, offset):
        ds = {}
        if (len(data) - offset) < (self.size()):
            return ds
        time_tup = struct.unpack_from('>HBB', data, offset)
        ds['current-utc-offset'] = time_tup[0]
        ds['current-utc-offset-valid'] = \
            'on' if (time_tup[1] & ptp4lconst.UTC_OFF_VALID) else 'off'
        ds['leap59'] = 'on' if (time_tup[1] & ptp4lconst.LEAP_59) else 'off'
        ds['leap61'] = 'on' if (time_tup[1] & ptp4lconst.LEAP_61) else 'off'
        ds['time-traceable'] = 'on' \
            if (time_tup[1] & ptp4lconst.TIME_TRACEABLE) else 'off'
        ds['freq-traceable'] = 'on' \
            if (time_tup[1] & ptp4lconst.FREQ_TRACEABLE) else 'off'
        if (time_tup[1] & ptp4lconst.PTP_TIMESCALE):
            ds['ptp-time-scale'] = 'on'
        else:
            ds['ptp-time-scale'] = 'off'
        if (time_tup[2] == 0):
            ds['time-source'] = 'atomic'
        elif (time_tup[2] == 1):
            ds['time-source'] = 'gnss'
        elif (time_tup[2] == 2):
            ds['time-source'] = 'terrestrial-radio'
        elif (time_tup[2] == 3):
            ds['time-source'] = 'serial-timecode'
        elif (time_tup[2] == 4):
            ds['time-source'] = 'ptp'
        elif (time_tup[2] == 5):
            ds['time-source'] = 'ntp'
        elif (time_tup[2] == 6):
            ds['time-source'] = 'handset'
        elif (time_tup[2] == 7):
            ds['time-source'] = 'other'
        elif (time_tup[2] == 8):
            ds['time-source'] = 'internal-oscillator'
        return ds


class PTPportDS:
    def __init__(self, port_id='00:00:00:00:00:00:00:00:00:00'):
        self.portId = port_id
        self.portState = 0
        self.logMinDelayReqInterval = 0
        self.peerMeanPathDelay = 0
        self.logAnnounceInterval = 0
        self.announceReceiptTimeout = 0
        self.logSyncInterval = 0
        self.delayMechanism = 0
        self.logMinPdelayReqInterval = 0
        self.versionNumber = 0

    def build(self):
        port_id = binascii.unhexlify(self.portId.replace(':', ''))
        port_ds = struct.pack('!10sBbQbBbBbB',
                              port_id,
                              self.portState,
                              self.logMinDelayReqInterval,
                              self.peerMeanPathDelay,
                              self.logAnnounceInterval,
                              self.announceReceiptTimeout,
                              self.logSyncInterval,
                              self.delayMechanism,
                              self.logMinPdelayReqInterval,
                              self.versionNumber,)
        return port_ds

    def size(self):
        return struct.calcsize('!10sBbQbBbBbB')

    def build_dict(self, data, offset):
        ds = {}
        par_port_id = ''
        if (len(data) - offset) < (self.size()):
            return ds
        t1 = struct.unpack_from('>10sBbQbBbBbB', data, offset)
        par_port_tup = struct.unpack_from('>BBBBBBBBBB', data, offset)
        for x in par_port_tup:
            par_port_id += '{0:02x}'.format(x)
        clkid_str = ':'.join(par_port_id[i:i + 2] for i in range(0, 16, 2))
        pid_str = ':'.join(par_port_id[i:i + 2] for i in range(16, 20, 2))
        portid_str = clkid_str + '-' + pid_str
        ds['port-id'] = portid_str
        if (t1[1] == 1):
            ds['port-state'] = 'initializing'
        elif (t1[1] == 2):
            ds['port-state'] = 'faulty'
        elif (t1[1] == 3):
            ds['port-state'] = 'disabled'
        elif (t1[1] == 4):
            ds['port-state'] = 'listening'
        elif (t1[1] == 5):
            ds['port-state'] = 'pre-master'
        elif (t1[1] == 6):
            ds['port-state'] = 'master'
        elif (t1[1] == 7):
            ds['port-state'] = 'passive'
        elif (t1[1] == 8):
            ds['port-state'] = 'uncalibrated'
        elif (t1[1] == 9):
            ds['port-state'] = 'slave'
        ds['delay-req-interval'] = t1[2]
        ds['peer-mean-path-delay'] = t1[3]
        ds['announce-interval'] = t1[4]
        ds['announce-timeout'] = t1[5]
        ds['sync-interval'] = t1[6]
        ds['delay-mechanism'] = t1[7]
        ds['peer-delay-req-interval'] = t1[8]
        ds['protocol-version'] = t1[9]

        return ds

    def __str__(self):
        return str(self.__class__) + ": " + str(self.__dict__)

class PTPportStatsNP:
    def __init__(self,
                 port_identity='00:00:00:00:00:00:00:00:00:00',
                 rx_sync=0,
                 tx_sync=0,
                 rx_delay_req=0,
                 tx_delay_req=0,
                 rx_peer_delay_req=0,
                 tx_peer_delay_rsp=0,
                 rx_rsvd4=0,
                 tx_rsvd4=0,
                 rx_rsvd5=0,
                 tx_rsvd5=0,
                 rx_rsvd6=0,
                 tx_rsvd6=0,
                 rx_rsvd7=0,
                 tx_rsvd7=0,
                 rx_followup=0,
                 tx_followup=0,
                 rx_delay_rsp=0,
                 tx_delay_rsp=0,
                 rx_pdelay_rsp_followup=0,
                 tx_pdelay_rsp_followup=0,
                 rx_announce=0,
                 tx_announce=0,
                 rx_signaling=0,
                 tx_signaling=0,
                 rx_management=0,
                 tx_management=0,):
        self.portIdentity = port_identity
        self.rxSync = rx_sync
        self.txSync = tx_sync
        self.rxDelayReq = rx_delay_req
        self.txDelayReq = tx_delay_req
        self.rxPeerDelayReq = rx_peer_delay_req
        self.txPeerDelayRsp = tx_peer_delay_rsp
        self.rxRsvd4 = rx_rsvd4
        self.txRsvd4 = tx_rsvd4
        self.rxRsvd5 = rx_rsvd5
        self.txRsvd5 = tx_rsvd5
        self.rxRsvd6 = rx_rsvd6
        self.txRsvd6 = tx_rsvd6
        self.rxRsvd7 = rx_rsvd7
        self.txRsvd7 = tx_rsvd7
        self.rxFollowup = rx_followup
        self.txFollowup = tx_followup
        self.rxDelayRsp = rx_delay_rsp
        self.txDelayRsp = tx_delay_rsp
        self.rxPDelayRspFollowup = rx_pdelay_rsp_followup
        self.txPDelayRspFollowup = tx_pdelay_rsp_followup
        self.rxAnnounce = rx_announce
        self.txAnnounce = tx_announce
        self.rxSignaling = rx_signaling
        self.txSignaling = tx_signaling
        self.rxManagement = rx_management
        self.txManagement = tx_management

    def build(self):
        port_id = binascii.unhexlify(self.portIdentity.replace(':', ''))
        port_ds = struct.pack('!10sQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ',
                              port_id,
                              self.portState,
                              self.rxSync,
                              self.txSync,
                              self.rxDelayReq,
                              self.txDelayReq,
                              self.rxPeerDelayReq,
                              self.txPeerDelayRsp,
                              self.rxRsvd4,
                              self.txRsvd4,
                              self.rxRsvd5,
                              self.txRsvd5,
                              self.rxRsvd6,
                              self.txRsvd6,
                              self.rxRsvd7,
                              self.txRsvd7,
                              self.rxFollowup,
                              self.txFollowup,
                              self.rxDelayRsp,
                              self.txDelayRsp,
                              self.rxPDelayRspFollowup,
                              self.txPDelayRspFollowup,
                              self.rxSignaling,
                              self.txSignaling,
                              self.rxManagement,
                              self.txManagement,)
        return port_ds

    def size(self):
        return struct.calcsize('!10sQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ')

    def build_dict(self, data, offset):
        par_port_id = ''
        stats = {}
        tup = struct.unpack_from('<10sQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ',
                                 data, offset)
        if (len(data) - offset) < (self.size()):
            return stats
        par_port_tup = struct.unpack_from('>BBBBBBBBBB', data, offset)
        for x in par_port_tup:
            par_port_id += '{0:02x}'.format(x)
        clkid_str = ':'.join(par_port_id[i:i + 2] for i in range(0, 16, 2))
        pid_str = ':'.join(par_port_id[i:i + 2] for i in range(16, 20, 2))
        portid_str = clkid_str + '-' + pid_str
        stats['port_id'] = portid_str
        stats['rx-sync'] = tup[1]
        stats['rx-delay-req'] = tup[2]
        stats['rx-peer-delay-req'] = tup[3]
        stats['rx-peer-delay-resp'] = tup[4]
        stats['rx-follow-up'] = tup[9]
        stats['rx-delay-resp'] = tup[10]
        stats['rx-delay-resp-follow-up'] = tup[11]
        stats['rx-announce'] = tup[12]
        stats['rx-signaling'] = tup[13]
        stats['rx-management'] = tup[14]

        stats['tx-sync'] = tup[17]
        stats['tx-delay-req'] = tup[18]
        stats['tx-peer-delay-req'] = tup[19]
        stats['tx-peer-delay-resp'] = tup[20]
        stats['tx-follow-up'] = tup[25]
        stats['tx-delay-resp'] = tup[26]
        stats['tx-delay-resp-follow-up'] = tup[27]
        stats['tx-announce'] = tup[28]
        stats['tx-signaling'] = tup[29]
        stats['tx-management'] = tup[30]

        return stats


class PTPportPropertiesNP:
    def __init__(self,
                 port_identity='00:00:00:00:00:00:00:00:00:00',
                 port_state=0,
                 timestamping=0,
                 text_length=0,
                 interface=''):
        self.portIdentity = port_identity
        self.port_state = port_state
        self.timestamping = timestamping
        self.textLength = text_length
        self.interface = interface
        return

    def size(self):
        return struct.calcsize('!10sBBB')

    def build_dict(self, data, offset):
        pnp = {}
        par_port_id = ''
        if (len(data) - offset) < (self.size()):
            return pnp
        tup = struct.unpack_from('<10sBBB',
                                 data, offset)
        par_port_tup = struct.unpack_from('>BBBBBBBBBB', data, offset)
        for x in par_port_tup:
            par_port_id += '{0:02x}'.format(x)
        pid_str = ':'.join(par_port_id[i:i + 2] for i in range(0, 20, 2))
        pnp['port-id'] = pid_str
        pnp['port-state'] = tup[1]
        pnp['timestamping'] = tup[2]
        if (tup[3] > 0):
            off = struct.calcsize('!10sBBB') + offset
            fm = '{:d}'.format(tup[3]) + 's'
            ptup_txt = struct.unpack_from(fm, data, off)
            pnp['name'] = ptup_txt[0].decode('ascii')
        else:
            pnp['name'] = 'Unknown'

        return pnp

    def __str__(self):
        return str(self.__class__) + ": " + str(self.__dict__)


class PTPportDSNP:
    def __init__(self,
                 nbr_prop_delay_thresh=0,
                 as_capable=0,):
        self.neighborPropDelayThresh = nbr_prop_delay_thresh
        self.asCapable = as_capable

    def size(self):
        return struct.calcsize('!LL')

    def build_dict(self, data, offset):
        ds = {}
        pup = struct.unpack_from('>LL', data, offset)
        ds['neighbor-prop-delay-thresh'] = pup[0]
        ds['as-capable'] = pup[1]
        return ds


class PTPforeignMastersDS:
    def __init__(self,
                 priority1=0,
                 priority2=0,
                 steps_removed=0,
                 is_gmc=0,
                 reserved=0,
                 identity='00:00:00:00:00:00:00:00',
                 clock_class=0,
                 clock_accuracy=0,
                 offset_scaled_log_variance=0,):
        self.priority1 = priority1
        self.priority2 = priority2
        self.stepsRemoved = steps_removed
        self.isGmc = is_gmc
        self.reserved = reserved
        self.identity = identity
        self.clockClass = clock_class
        self.clockAccuracy = clock_accuracy
        self.offsetScaledLogVariance = offset_scaled_log_variance

    def size(self):
        return struct.calcsize('!BBHB3B8sBBH')

    def build_dict(self, data, offset):
        ds = {}
        clk_quality = {}
        fup = struct.unpack_from('>BB', data, offset)
        ds['priority1'] = fup[0]
        ds['priority2'] = fup[1]

        off = offset + 2
        clkid_tup = struct.unpack_from('>BBBBBBBBB', data, off)
        clkid = ''
        for x in clkid_tup:
            clkid += '{0:02x}'.format(x)
        clkid_str = ':'.join(clkid[i:i + 2] for i in range(0, 16, 2))
        ds['clock-id'] = clkid_str

        off = off + 8
        clq = struct.unpack_from('>BBHHBL', data, off)
        clk_quality['clock-class'] = clq[0]
        clk_quality['clock-accuracy'] = clq[1]
        clk_quality['offset-scaled-log-variance'] = clq[2]
        ds['clock-quality'] = clk_quality
        ds['steps-removed'] = clq[3]
        ds['is-grandmaster-clock'] = 'on' if (clq[4] == 1) else 'off'
        ds['if-index'] = clq[5]

        return ds


class PTPAcceptableMastersTbl:
    def __init__(self,
                 tbl_size=0,
                 clock_id='00:00:00:00:00:00:00:00',
                 alt_priority=0):
        self.tableSize = tbl_size
        self.clock_id = clock_id
        self.alt_priority = alt_priority
        return

    def size(self):
        return struct.calcsize('!H')

    def build_dict(self, data, offset):
        ds = {}
        if len(data) >= 2:
            tbl_size = aup = struct.unpack_from('<H', data, offset)
            if tbl_size[0] == 0:
                return ds
        else:
            return ds

        aup = struct.unpack_from('<HBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB',
                                 data, offset)
        self.tableSize = aup[0]
        off = offset + 2
        i = 0
        while i < self.tableSize:
            clk_tup = struct.unpack_from('<BBBBBBBB', data, off)
            clk_id = ''
            for x in clk_tup:
                clk_id += '{0:02x}'.format(x)
            clkid_str = ':'.join(clk_id[i:i + 2] for i in range(0, 16, 2))
            off += 8
            ap_tup = struct.unpack_from('<B', data, off)
            off += 4
            i += 1
            ds[clkid_str] = ap_tup[0]
        return ds

class PTPForcedMastersLog:
    def __init__(self, ):
        return

    def size(self):
        return struct.calcsize('>BBBBBBBBLQQLHHL16s8s16s')

    def build_dict(self, data, offset):
        log = {}
        ds = {}
        length = len(data)
        if length == 0:
            return log

        off = offset
        idx = 1
        while length > self.size():
            clkid_tup = struct.unpack_from('>BBBBBBBB', data, off)
            clkid = ''
            for x in clkid_tup:
                clkid += '{0:02x}'.format(x)
            clkid_str = ':'.join(clkid[i:i + 2] for i in range(0, 16, 2))
            log['clock-id'] = clkid_str

            off = offset + 8
            tup = struct.unpack_from('<LQQLHHL16s8s16s', data, off)

            dt = datetime.fromtimestamp(int(tup[1]), tz=timezone.utc)
            st = dt.strftime('%Y-%m-%dT%H:%M:%S')
            log['last-time'] = st + "." + str(tup[2]) + 'Z'
            log['count'] = int(tup[3])
            sa_family = tup[4]
            if (sa_family == 2):
                addr = (ipaddress.ip_address(socket.ntohl(tup[6])))
                log['source-address'] = str(addr)
            elif (sa_family == 10):
                log['source-address'] = tup[7]
            else:
                log['source-address'] = tup[8]
            log['interface'] = tup[9].decode('ascii').rstrip('\u0000')

            ds[idx] = log
            length = length - self.size()
            idx = idx + 1

        return ds


class PTPAcceptableMastersLog:
    def __init__(self, ):
        return

    def size(self):
        return struct.calcsize('>BBBBBBBBLQQLHHL16s8s16s')

    def build_dict(self, data, offset):
        log = {}
        ds = {}
        length = len(data)
        if length == 0:
            return log

        off = offset
        idx = 1
        while length > self.size():
            clkid_tup = struct.unpack_from('>BBBBBBBB', data, off)
            clkid = ''
            for x in clkid_tup:
                clkid += '{0:02x}'.format(x)
            clkid_str = ':'.join(clkid[i:i + 2] for i in range(0, 16, 2))
            log['clock-id'] = clkid_str

            off = offset + 8
            tup = struct.unpack_from('<LQQLHHL16s8s16s', data, off)

            dt = datetime.fromtimestamp(int(tup[1]), tz=timezone.utc)
            st = dt.strftime('%Y-%m-%dT%H:%M:%S')
            log['last-time'] = st + "." + str(tup[2]) + 'Z'
            log['count'] = int(tup[3])
            sa_family = tup[4]
            if (sa_family == 2):
                addr = (ipaddress.ip_address(socket.ntohl(tup[6])))
                log['source-address'] = str(addr)
            elif (sa_family == 10):
                log['source-address'] = tup[7]
            else:
                log['source-address'] = tup[8]
            log['interface'] = tup[9].decode('ascii').rstrip('\u0000')

            ds[idx] = log
            length = length - self.size()
            idx = idx + 1

        return ds


def ptp_do_get(ptp_domain, tgt_port, mgmt_id):

    global ptp_sequence_id

    # Create a UDS socket
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)

    try:
        os.unlink(ptp4lconst.client_address)
    except OSError:
        if os.path.exists(ptp4lconst.client_address):
            raise

    sock.bind(ptp4lconst.client_address)

    ptp_sequence_id = ptp_sequence_id + 1

    # Build PTP message - Header part
    ptp_hdr = PTPHeader(ptp4lconst.PTP_MSG_MANAGEMENT,
                        ptp4lconst.PTP_VERSION,
                        struct.calcsize('!BBHBBHQLBBBBBBBBHHBB'),
                        ptp_domain,
                        ptp4lconst.PTP_SOURCE_PORT_ID,
                        ptp_sequence_id,
                        ptp4lconst.PTP_MSG_CTL_MANAGEMENT, 0x7F)

    # Build PTP message - Management message part
    mgmt_msg = PTPManagementMsg(tgt_port,
                                ptp4lconst.PTP_DEFAULT_BNDRY_HOPS,
                                ptp4lconst.PTP_DEFAULT_BNDRY_HOPS,
                                ptp4lconst.PTP_MANAGEMENT_ACTION_GET)

    # Build PTP message - Command specific PTP message part
    if (mgmt_id == ptp4lconst.TLV_DEFAULT_DATA_SET):
        cmd = PTPdefaultDS()
        data = cmd.build()
        cmdsz = cmd.size()
    elif (mgmt_id == ptp4lconst.TLV_PARENT_DATA_SET):
        cmd = PTPparentDS()
        data = cmd.build()
        cmdsz = cmd.size()
    elif (mgmt_id == ptp4lconst.TLV_CURRENT_DATA_SET):
        cmd = PTPcurrentDS()
        data = cmd.build()
        cmdsz = cmd.size()
    elif (mgmt_id == ptp4lconst.TLV_TIME_PROPERTIES_DATA_SET):
        cmd = PTPtimePropertiesDS()
        data = cmd.build()
        cmdsz = cmd.size()
    elif (mgmt_id == ptp4lconst.TLV_PORT_DATA_SET):
        cmd = PTPportDS(tgt_port)
        data = cmd.build()
        cmdsz = cmd.size()
    elif (mgmt_id == ptp4lconst.TLV_PORT_STATS_NP):
        cmd = PTPportStatsNP()
        data, cmdsz = '', 0
    elif (mgmt_id == ptp4lconst.TLV_PORT_PROPERTIES_NP):
        cmd = PTPportPropertiesNP()
        data, cmdsz = '', 0
    elif (mgmt_id == ptp4lconst.TLV_PORT_DATA_SET_NP):
        cmd = PTPportDSNP()
        data, cmdsz = '', 0
    elif (mgmt_id == ptp4lconst.TLV_FOREIGN_MASTERS_DATA_SET):
        cmd = PTPforeignMastersDS()
    elif (mgmt_id == ptp4lconst.TLV_ACCEPTABLE_MASTER_TABLE):
        cmd = PTPAcceptableMastersTbl()
        data, cmdsz = '', 0
    elif (mgmt_id == ptp4lconst.TLV_FORCED_MASTER_LOG_NP):
        cmd = PTPForcedMastersLog()
        data, cmdsz = '', 0
    elif (mgmt_id == ptp4lconst.TLV_ACCEPTABLE_MASTER_LOG_NP):
        cmd = PTPAcceptableMastersLog()
        data, cmdsz = '', 0

    # Build PTP message - Management tlv part
    mgmt_tlv = PTPManagementTLV(ptp4lconst.TLV_MANAGEMENT, cmdsz + 2, mgmt_id)

    msg_len = ptp_hdr.size() + mgmt_msg.size() + mgmt_tlv.size()
    if cmdsz != 0:
        msg_len += cmdsz
        setattr(ptp_hdr, 'messageLength', msg_len)
        mgt_pkt = ptp_hdr.build() + mgmt_msg.build() + mgmt_tlv.build() + data
    else:
        setattr(ptp_hdr, 'messageLength', msg_len)
        mgt_pkt = ptp_hdr.build() + mgmt_msg.build() + mgmt_tlv.build()

    sock.settimeout(5)
    sock.sendto(mgt_pkt, ptp4lconst.server_address)

    hdr_size = ptp_hdr.size() + mgmt_msg.size() + mgmt_tlv.size()

    try:
        data, address = sock.recvfrom(1024)
    except socket.timeout:
        return {}

    sock.close()
    d = cmd.build_dict(data, hdr_size)
    return d


def ptp4lsys_port_map_build(ptp_domain):
    portid = ptp_build_portid(ptp4lconst.PTP_DEFAULT_CLOCK_ID,
                              ptp4lconst.PTP_TGT_PORT_ALL)
    def_ds = ptp_do_get(ptp_domain, portid,
                        ptp4lconst.TLV_DEFAULT_DATA_SET)
    num_ports = def_ds['number-ports']
    for port in range(1, num_ports + 1):
        portid = ptp_build_portid(def_ds['clock-id'], port)
        pp_ds = ptp_do_get(ptp_domain, portid,
                           ptp4lconst.TLV_PORT_PROPERTIES_NP)
        ptp4lsys_port_map[pp_ds['name']] = pp_ds


def get_ptp4l_portid(ptp_domain, port_name):
    # Portmap is built if port_name is in the portmap else rebuild
    if port_name not in ptp4lsys_port_map:
        ptp4lsys_port_map_build(ptp_domain)

    # Portmap is rebuilt check if port_name exists
    if port_name in ptp4lsys_port_map:
        pp_ds = ptp4lsys_port_map[port_name]
        return pp_ds['port-id']
    return ''


if __name__ == '__main__':
    ds = None

    try:
        # TODO Use argcomplete or something like it.
        if len(sys.argv) == 3:
            ptp_domain = int(sys.argv[2])
            if sys.argv[1] == 'current':
                ds = ptp_do_get(ptp_domain, ptp4lconst.PTP_ALL_PORT_ID,
                                ptp4lconst.TLV_CURRENT_DATA_SET)
            elif sys.argv[1] == 'parent':
                ds = ptp_do_get(ptp_domain, ptp4lconst.PTP_ALL_PORT_ID,
                                ptp4lconst.TLV_PARENT_DATA_SET)
            elif sys.argv[1] == 'props':
                ds = ptp_do_get(ptp_domain, ptp4lconst.PTP_ALL_PORT_ID,
                                ptp4lconst.TLV_TIME_PROPERTIES_DATA_SET)
            elif sys.argv[1] == 'master':
                ds = ptp_do_get(ptp_domain, ptp4lconst.PTP_ALL_PORT_ID,
                                ptp4lconst.TLV_ACCEPTABLE_MASTER_TABLE)
            elif sys.argv[1] == 'default' or sys.argv[1] == 'ptps-ds':
                ds = ptp_do_get(ptp_domain, ptp4lconst.PTP_ALL_PORT_ID,
                                ptp4lconst.TLV_DEFAULT_DATA_SET)
        elif len(sys.argv) == 4:
            ptp_domain = int(sys.argv[3])
            if sys.argv[1] == 'port':
                # ptp4l-cli port <port> <domain>
                tgt_port = get_ptp4l_portid(ptp_domain, sys.argv[2])
                if tgt_port == '':
                    ds = {}
                    tgt_port = ptp4lconst.PTP_ALL_PORT_ID
                ds = ptp_do_get(ptp_domain, tgt_port,
                                ptp4lconst.TLV_PORT_DATA_SET)
            elif sys.argv[1] == 'stats':
                # ptp4l-cli stats <port> <domain>
                tgt_port = get_ptp4l_portid(ptp_domain, sys.argv[2])
                if tgt_port == 0:
                    ds = {}
                    tgt_port = ptp4lconst.PTP_ALL_PORT_ID
                ds = ptp_do_get(ptp_domain, tgt_port,
                                ptp4lconst.TLV_PORT_STATS_NP)
            elif sys.argv[1] == 'log':
                # ptp4l-cli log <type> <domain>
                tgt_port = ptp4lconst.PTP_ALL_PORT_ID
                ds = {}
                if sys.argv[2] == 'fm':
                    ds = ptp_do_get(ptp_domain, tgt_port,
                                ptp4lconst.TLV_FORCED_MASTER_LOG_NP)
                elif sys.argv[2] == 'am':
                    ds = ptp_do_get(ptp_domain, tgt_port,
                                ptp4lconst.TLV_ACCEPTABLE_MASTER_LOG_NP)

    except Exception:
        # TODO Actual error handling
        print("Failed request")  # noqa
        exit(1)

    if ds is None:
        # TODO Useful help text
        print("Bad request")  # noqa
        exit(1)

    # TODO support output other than JSON
    print(json.dumps(ds))  # noqa
