#!   /usr/bin/env   python
#    coding: utf8
##_________________________________________________________________________________________________
##                                                                                                |
##                                           |TDC PTS|                                            |
##                                                                                                |
##                                         CERN,BE/CO-HT                                          |
##________________________________________________________________________________________________|

##-------------------------------------------------------------------------------------------------
##                                                                                                |
##                                  TDC Calibration Verification                                  |
##                                                                                                |
##-------------------------------------------------------------------------------------------------
##                                                                                                |
## Description  The test reads the calibration data from the TDC mezzanine EEPROM and confirms    |
##              their validity. First the DAC gets calibrated with the retreived EEPROM value     |
##              and through pulses of 9 ms period, similarly to the DAC_calibration.py, we verify |
##              its performance. For the channel differences, we also retrive the EEPROM          |
##              calibration values, send pulses to the different channel pairs and validate the   |
##              performance.                                                                      |
##                                                                                                |
##                                                                                                |
## FW to load   .bin                                                                              |
## Authors      Evangelia Gousiou (Evangelia.Gousiou@cern.ch)                                     |
## Website      http://www.ohwr.org/projects/pts                                                  |
## Date         11/01/2013                                                                        |
##-------------------------------------------------------------------------------------------------

##-------------------------------------------------------------------------------------------------
##                               GNU LESSER GENERAL PUBLIC LICENSE                                |
##                              ------------------------------------                              |
## This source file is free software; you can redistribute it and/or modify it under the terms of |
## the GNU Lesser General Public License as published by the Free Software Foundation; either     |
## version 2.1 of the License, or (at your option) any later version.                             |
## This source is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;       |
## without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.      |
## See the GNU Lesser General Public License for more details.                                    |
## You should have received a copy of the GNU Lesser General Public License along with this       |
## source; if not, download it from http://www.gnu.org/licenses/lgpl-2.1.html                     |
##-------------------------------------------------------------------------------------------------

##-------------------------------------------------------------------------------------------------
##                                            Import                                             --
##-------------------------------------------------------------------------------------------------


# Import system modules
import sys
import time
import os
import math
from numpy import *
from ctypes import *
from datetime import datetime

# Add common modules location tp path
sys.path.append('../../../../pts/')
sys.path.append('../../../../pts/gnurabbit/python/')
sys.path.append('../../../../pts/common/')
sys.path.append('../../../../pts/common/usb_box')
sys.path.append('../../../../pts/common/cp210x')



# Import common modules
from ptsexcept import *
import rr
import csr
import fmc_tdc
from PendulumCNT91 import *
from calibr_box import *
from cp210x_eeprom import *
from find_usb_tty import *

PENDULUM           = "/dev/usbtmc1"
USB_DEVICE         = "/dev/ttyUSB0"
RS232_BAUD         = 57600

BOX_USB_VENDOR_ID  = 0x10c4 # Cygnal Integrated Products, Inc.
BOX_USB_DEVICE_ID  = 0xea60 # CP210x Composite Device

NUM_OF_TSTAMPS     = 10     # number of fill-ups of the TDC memory
                            # ex NUM_OF_TSTAMPS = 10, means aquisition
                            # of 2560 timestamps; in this setup this means
                            # 640 rising edge timestamps per channel

EEPROM_ADDR        = 0x50

PENTULUM_PERIOD_S  = 0.005
PENTULUM_PERIOD_PS = 0.005*1e12
PENTULUM_WIDTH_S   = 0.0001


def pendulum_output_on(meas):
    meas.write("OUTP:TYPE PULS")
    meas.write("SOUR:PULS:PER 0.005")   #PENTULUM_PERIOD_S
    meas.write("SOUR:PULS:WIDT 0.0001") #PENTULUM_WIDTH_S
    return (1)

def pendulum_output_off(meas):
    meas.write("OUTP:TYPE OFF")
    return (1)

# Converts two's complement hex to signed
def hex2signed(value):
    if(value & 0x80000000):
        return -((~value & 0xFFFFFFFF) + 1)
    else:
        return value

##-------------------------------------------------------------------------------------------------
##                                             Main                                              --
##-------------------------------------------------------------------------------------------------

def main (default_directory='.'):

    # Constants declaration
    FMC_TDC_ADDR             = '1a39:0004/1a39:0004@000B:0000'
    FMC_TDC_BITSTREAM_PATH   = '../firmwares/tdc.bin'
    FPGA_LOADER_PATH         = '../../../../pts/gnurabbit/user/fpga_loader'

    CALIBR_BIN_FILENAME = "calib_data.bin"
    CALIBR_BIN_FILENAME = os.path.join(default_directory, CALIBR_BIN_FILENAME)
    EEPROM_BIN_FILENAME = "eeprom_content.out"
    EEPROM_BIN_FILENAME = os.path.join(default_directory, EEPROM_BIN_FILENAME)
    EEPROM_SIZE = 8192 # in Bytes
    start_test_time = time.time()

    # SPEC object declaration
    spec = rr.Gennum()

    # Bind SPEC object to FMC TDC card
    print "\n-------------------------------------------------------------------"
    print "----------------------------- FMC TDC -----------------------------"
    print "-------------------- Calibration Verification ---------------------\n"

    print ("The test reads the calibration data from the TDC mezzanine EEPROM\n"
           "and configures the TDC accordingly; it then sends few pulses to the\n"
           "TDC channels and roughly confirms the calibration data validity.\n")


    print "\n_______________________________Info______________________________\n"
    print "FMC TDC address to parse: %s"%(FMC_TDC_ADDR)
    for name, value in spec.parse_addr(FMC_TDC_ADDR).iteritems():
        print "%s:0x%04X"%(name, value)
    spec.bind(FMC_TDC_ADDR)

    # Load FMC TDC firmware
    print "\n_________________________Initialisations_________________________\n"
    print "Loading FMC TDC firmware...",
    firmware_loader = os.path.join(default_directory, FPGA_LOADER_PATH)
    bitstream = os.path.join(default_directory, FMC_TDC_BITSTREAM_PATH)
    os.system(firmware_loader + ' ' + bitstream)
    time.sleep(1)
    print "Firmware loaded!"

    # TDC object declaration
    tdc = fmc_tdc.CFMCTDC(spec)

    # Check bitsteam type
    bitstream_type = tdc.read_bitstream_type()
    if(bitstream_type == 0xFFFFFFFF):
        msg = ("FATAL ERROR: No access to TDC core")
        print (msg)
        raise PtsError (msg)
    if(bitstream_type != 0x2):
        msg = ("FATAL ERROR: Wrong bitstream type loaded 0x%08X") %bitstream_type
        print (msg)
        raise PtsError (msg)
    else:
        print('Access to TDC core OK')

    # Configure ACAM and TDC core registers
    print "\n__________________________Configuration__________________________\n"
    tdc.config_acam()
    time.sleep(1)
    tdc.reset_acam()
    acam_status_test = tdc.read_acam_status()-0xC4000800
    if acam_status_test == 0:
        print "ACAM IC8: Status register OK"
    else:
        msg = ("ERROR: ACAM IC8: No communication")
        print (msg)
        raise PtsError (msg)
    acam_readback_regs = []
    acam_readback_regs = tdc.readback_acam_config()
    for i in range(len(tdc.ACAM_READBACK_REGS)): 
        if (acam_readback_regs[i] == tdc.ACAM_READBACK_REGS[i]):
            print "ACAM IC8: reg 0x%02X: 0x%08X OK"%(tdc.ACAM_READBACK_ADDR[i], acam_readback_regs[i])
        else:
            msg = ("ERROR: ACAM IC8: Configuration registers failure; reg 0x%2X: received 0x%8X, expected 0x%8X"%(tdc.ACAM_READBACK_ADDR[i], acam_readback_regs[i], tdc.ACAM_READBACK_REGS[i]))
            print (msg)
            raise PtsError (msg)

    # Interrupts
    print "\n____________________________IRQs_________________________________\n"
    print('Set IRQ enable mask: %.4X')%tdc.set_irq_en_mask(0x1F) # was 0xC!!
    tdc.set_irq_tstamp_thresh(0x100)#100
    tdc.set_irq_time_thresh(0xFFFFF)

    # Communication and Setup of Interconnection box
    print "\n_____________________USB interconnection box______________________\n"
    box_tty = CttyUSB().find_usb_tty(BOX_USB_VENDOR_ID, BOX_USB_DEVICE_ID)
    print("Device: %s" % box_tty[0])
    box = CCalibr_box(box_tty[0])
    box.select_trigger_loopback(0)
    box.select_output(4)
    # Retrieval of calibration values from USB box eeprom
    box_eeprom = CCP210x_Eeprom("%X"%BOX_USB_VENDOR_ID, "%X"%BOX_USB_DEVICE_ID)
    print "\nReading USB interconnection box calibration data from cp210x EEPROM.."
    box_calibration       = box_eeprom.get_calibr_data()
    box_calibration_float = []
    for channel, value in box_calibration.iteritems():
        print "%s calibration value is: %s ps" % (channel, value)
        if float(value) > 1000 or float(value) < -1000:
            msg = ('WARNING: USB interconnection box calibration value %s: %.2f ps does not look right.\n'
                   'A value between [-1000 ps .. 1000 ps] was expected')% (channel, float(value))
            print (msg)
            raise  PtsWarning (msg)
        box_calibration_float.append(float(value))
    box_calibration_float.reverse()
    print box_calibration_float

    # Communication with Pendulum
    print "\n__________________________USB Pendulum_____________________________\n"
    try:
        meas = PendulumCNT91(PENDULUM)
    except OSError:
        raise PtsError('CNT-91 is not switched on or badly connected, aborting.')
    a = pendulum_output_off(meas)

    # Get EEPROM calibration data
    print "\n_____________________EEPROM calibration data_______________________\n"
    # Read entire EEPROM
    print("Reading all eeprom content..")
    eeprom_data_read = tdc.mezz_i2c_eeprom_read(0, EEPROM_SIZE)

    # Write EEPROM data to binary file
    print("Writing eeprom content to binary file: %s"%(EEPROM_BIN_FILENAME))
    f_eeprom = open(EEPROM_BIN_FILENAME, "wb")
    for byte in eeprom_data_read:
        f_eeprom.write(chr(byte))
    f_eeprom.close()

    # Get calibration data
    #print("Extract calibration binary file to: %s"%(CALIBR_BIN_FILENAME))
    cmd = 'sdb-read -e 0x200 ' + EEPROM_BIN_FILENAME + ' calib > ' + CALIBR_BIN_FILENAME
    print("Exctracting calibration binary file, cmd: %s"%(cmd))
    os.system(cmd)
    print "Getting calibration data from binary file."
    calibr_data = []
    f_calibr_data = open(CALIBR_BIN_FILENAME, "rb")
    try:
        byte = f_calibr_data.read(1)
        while byte != "":
            calibr_data.append(ord(byte))
            byte = f_calibr_data.read(1)
    finally:
        f_eeprom.close()

    # Re-arrange calibration data into 32-bit numbers (from bytes)
    eeprom_corr_data = []
    for i in range(0,len(calibr_data)-3,4):
        eeprom_corr_data.append((calibr_data[i+3] << 24)+(calibr_data[i+2] << 16)+(calibr_data[i+1] << 8) + (calibr_data[i]))
        print "0x%08X" % eeprom_corr_data[-1]
    print "Calibration data length (32-bit): %d" % len(eeprom_corr_data)

    print "Calibration data from eeprom:"
    ch_calibration   = []
    dac_value        = eeprom_corr_data[4]
    for i in range(4):
        ch_calibration.append(float((hex2signed(eeprom_corr_data[i])))/100)
    print ("Ch1-Ch2 calibration value: %.2f ps") %ch_calibration[0]
    print ("Ch1-Ch3 calibration value: %.2f ps") %ch_calibration[1]
    print ("Ch1-Ch4 calibration value: %.2f ps") %ch_calibration[2]
    print ("Ch1-Ch5 calibration value: %.2f ps") %ch_calibration[3]
    print ("DAC calibration value: 0x%8X")  %dac_value

    # DAC
    print "\n\n________________________DAC Configuration___________________________\n"
    tdc.configure_mezz_dac(dac_value)
    time.sleep(0.5)

    print "TDC Write pointer: %d"%(tdc.get_pointer())
    print "TDC Overflows    : %d"%(tdc.get_overflow_counter())

    # Enable channels and start timestamps aquisition (TDC_START_FPGA pulse sent at this step) 
    print "\n\n______________________ACAM aquisition start________________________\n"

    # Enable channels and terminations of all channels
    tdc.enable_channels()
    tdc.channel_term(1, 1)
    tdc.channel_term(2, 1)
    tdc.channel_term(3, 1)
    tdc.channel_term(4, 1)
    tdc.channel_term(5, 1)

    tdc.start_acq()
    time.sleep(0.5)
    print "\n-----------------------------------------------------------------\n"

    all_avg_corr = []
    all_data     = []
    timestamps   = []
    data         = []

    #------------> Channels calibration verification
    for i in range(4): # for the 4 pairs of channels (1,2), (1,3), (1,4), (1,5)

        otherch                   = i+1
        r_edge_timestamps_ch1     = []
        r_edge_timestamps_otherch = []
        all_measurs               = []
        all_r_edge_timestamps     = []

        print "Ch1-Ch%d"%(i+2)

        # connect TDC channels to fanout outputs 
        box.select_trigger_loopback(0)
        box.select_output(4-i)

        # pendulum sending pulses
        a = pendulum_output_on(meas)

        # TDC aquisition of NUM_OF_TSTAMPS*64 timestamps per channel
        for j in range(NUM_OF_TSTAMPS):
            tdc.check_irq(0)
            timestamps, data = tdc.get_timestamps(0)
            # keep only rising edge timestamps
            for m in range(len(timestamps)):
                if (timestamps[m][2] == 1)   and (timestamps[m][1] == 0):       # rising tstamps ch1
                    r_edge_timestamps_ch1.append(timestamps[m][0])
                elif (timestamps[m][2] == 1) and (timestamps[m][1] == otherch): # rising tstamps pair channel
                    r_edge_timestamps_otherch.append(timestamps[m][0])
            # clear interrupt
            tdc.clear_irq_src(int(math.log(tdc.get_irq_src(),2)))

        # pendulum output off
        a = pendulum_output_off(meas)


        # data manipulation

        # keep only rising edge timestamps
        tdc_write_pointer_now = tdc.get_pointer()
        tdc_overflows_now     = tdc.get_overflow_counter()
        print "\nTDC Write pointer: %d"%(tdc_write_pointer_now)
        print "TDC Overflows    : %d"%(tdc_overflows_now)
        if tdc_overflows_now != (i+1)*NUM_OF_TSTAMPS:
            msg = ("ERROR: Unexpected error with interrupts; expected overflows: %d; received overflows: %d")%((i+1)*NUM_OF_TSTAMPS,tdc_overflows_now)
            print (msg)
            raise PtsError (msg)
        timestamps, data = tdc.get_timestamps(0)
        for m in range(len(timestamps)):
            if (timestamps[m][2] == 1)   and (timestamps[m][1] == 0):       # rising tstamps ch1
                r_edge_timestamps_ch1.append(timestamps[m][0])
            elif (timestamps[m][2] == 1) and (timestamps[m][1] == otherch): # rising tstamps pair channel
                r_edge_timestamps_otherch.append(timestamps[m][0])

        print "Ch1 tstamps      : %d"%(len(r_edge_timestamps_ch1))
        print "Ch%d tstamps      : %d"%(i+2, len(r_edge_timestamps_otherch))

        if len(r_edge_timestamps_ch1) > 63*NUM_OF_TSTAMPS:
            r_edge_timestamps_ch1.sort()
        else:
            msg = ("ERROR: Ch1 registered timestamps: %d; expected: %d \n")%(len(r_edge_timestamps_ch1), 64*NUM_OF_TSTAMPS)
            print (msg)
            raise PtsError (msg)

        if len(r_edge_timestamps_otherch) > 63*NUM_OF_TSTAMPS:
            r_edge_timestamps_otherch.sort()
        else:
            msg = ("ERROR: Ch%d registered timestamps: %d; expected: %d \n")%(i+2, len(r_edge_timestamps_otherch), 64*NUM_OF_TSTAMPS)
            print (msg)
            raise PtsError (msg)

        # allign lists with rising-edge-timestamps for ch1 and the other channel
        offset = 0
        for l in range(10):
            if (r_edge_timestamps_ch1[l] - r_edge_timestamps_otherch[0]) < 10000 and (r_edge_timestamps_ch1[l] - r_edge_timestamps_otherch[0]) > -10000:
                offset= l
                print "offset           : %d"%(l)
        for k in range(0,min(len(r_edge_timestamps_otherch),len(r_edge_timestamps_ch1))-offset,1):
            measur = r_edge_timestamps_ch1[k+offset] - r_edge_timestamps_otherch[k]
            all_measurs.append(measur)

        # check the timestamps to exclude possible +-4ns error 
        all_measurs_check = []
        for m  in range(0,len(all_measurs)-1,1):
            if abs(all_measurs[m] - all_measurs[m+1]) < 700:
                all_measurs_check.append(all_measurs[m])

        avg_raw = (sum(all_measurs_check, 0.0) / len(all_measurs_check))
        print "samples          : %d"%(len(all_measurs_check))
        print "average raw      : %.2f ps"%(avg_raw)
        print "span             : %.2f ps"%((max(all_measurs_check))-(min(all_measurs_check)))
        print "max iter %d %.2f ps"%(all_measurs_check.index(max(all_measurs_check)), max(all_measurs_check))
        print "min iter %d %.2f ps"%(all_measurs_check.index(min(all_measurs_check)),min(all_measurs_check))

        avg_corr = (sum(all_measurs_check, 0.0) / len(all_measurs_check))-ch_calibration[i] +box_calibration_float[i]
        print "average corrected: %.2f ps"%(avg_corr)
        all_avg_corr.append(avg_corr)


        if i == 0:
            period_measurs = []
            for k in range(0,len(r_edge_timestamps_ch1)-1,1):
                period = r_edge_timestamps_ch1[k+1] -r_edge_timestamps_ch1[k]
                if (period > 4000000000 and period < 6000000000):
                    period_measurs.append(period)

        print "-----------------------------------------------------------------\n\n"

    print "\n__________________Channels Calibration Evaluation_________________\n"
    msg = ""
    span = abs(max(all_avg_corr) - min(all_avg_corr))
    if span < 80:
        print("The measurements span %.2f ps is less than the accepted 80 ps:")%span
        print("[ch1-ch2:%.2f ps, ch1-ch3:%.2f ps, ch1-ch4:%.2f ps, ch1-ch5:%.2f ps].")%(all_avg_corr[0], all_avg_corr[1], all_avg_corr[2], all_avg_corr[3])
        print("\nChannels Calibration OK!")
    else:
        msg = ('ERROR: Channels Calibration Failed; the measurements span %.2f is higher than 80 ps\n'
               'Input-cables-difference measured by ch1-ch2: %.2f\n'
               'Input-cables-difference measured by ch1-ch3: %.2f\n'
               'Input-cables-difference measured by ch1-ch4: %.2f\n'
               'Input-cables-difference measured by ch1-ch5: %.2f')%(span, all_avg_corr[0], all_avg_corr[1], all_avg_corr[2], all_avg_corr[3])
 
    print "\n\n____________________DAC Calibration Evaluation____________________\n"

    print "samples for DAC evaluation : %d"%(len(period_measurs))
    print "expected period            : %.2f ps"%PENTULUM_PERIOD_PS
    period_avg = sum(period_measurs)/len(period_measurs)
    print "measured period average    : %.2f ps"%(period_avg)
    print "measured period span       : %.2f ps"%((max(period_measurs))-(min(period_measurs)))
    print "measured period max iter %d %.2f ps"%(period_measurs.index(max(period_measurs)), max(period_measurs))
    print "measured period min iter %d %.2f ps"%(period_measurs.index(min(period_measurs)),min(period_measurs))
    error= abs(PENTULUM_PERIOD_PS - period_avg)
    print "error                      : %.2f ps"%(error)
    if error < 80:
        print("\nDAC Calibration OK!\n")
    else:
        msg += (("\nERROR: DAC Calibration Failed; the measurements error %.2f ps is higher than +-40 ps.\n")%error)

    if msg != "":
        print (msg)
        raise PtsError (msg)


    print "\n\n--------------------------------"
    end_test_time = time.time()
    print "Test elapsed time: %.2f seconds\n\n\n" % (end_test_time-start_test_time)


if __name__ == '__main__' :
    main()
