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

##-------------------------------------------------------------------------------------------------
##                                                                                                |
##                              USB-Interconnections-Box Calibration                              |
##                                                                                                |
##-------------------------------------------------------------------------------------------------
##                                                                                                |
## Description  The board HCCFFIC___-CR000001 is essential for this calibration procedure.        |
##                                                                                                |
##                                                                                                |
## 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 pylab import *
from datetime import datetime

# Add common modules location tp path
sys.path.append('../../../../')
sys.path.append('../../../../gnurabbit/python/')
sys.path.append('../../../../common/')
sys.path.append('../../../../common/usb_box')
sys.path.append('../../../../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

NUM_OF_TSTAMPS    = 1000

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

HCCFFIC_CR000001_CALIB = [-145.07, 381.21, 356.53, 283.28]#[-146.29, 379.38, 356.69, 280.73]
CALIBR_CHANNELS =  ['ch1-ch2', 'ch1-ch3', 'ch1-ch4', 'ch1-ch5']

def pendulum_output_on(meas):
    meas.write("OUTP:TYPE PULS")
    meas.write("SOUR:PULS:PER 0.001")
    meas.write("SOUR:PULS:WIDT 0.0005")
    return (1)

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

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

def main (default_directory='.'):

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

    CH_CALIBR_FILENAME = "ch_calibration_data.txt"
    CH_CALIBR_FILENAME = os.path.join(default_directory, CH_CALIBR_FILENAME)
    f_out = open(CH_CALIBR_FILENAME, 'w')
    start_test_time = time.time()

    # SPEC object declaration
    spec = rr.Gennum()

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

    print ("Calibration of a USB-interconnection-box. Use only the TDC board\n"
           "HCCFFIC___-CR00000001!!.\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)
    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)

    # DAC
    tdc.configure_mezz_dac(0xA9CF)

    # Interrupts
    print "\n____________________________IRQs_________________________________\n"
    print('Set IRQ enable mask: %.4X')%tdc.set_irq_en_mask(0x1F)
    tdc.set_irq_tstamp_thresh(0x100)
    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 "Reading USB-interconnection-box existing 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\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)

    # 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)

    # Enable timestamps aquisition (TDC_START_FPGA pulse sent at this step) 
    print "\n______________________ACAM aquisition start________________________\n"
    tdc.start_acq()
    print "\n-----------------------------------------------------------------\n\n\n"

    all_avg             = []
    all_avg_cables_swap = []

    #------------> loop with cables straight
    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               = []

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

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

        # 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 != NUM_OF_TSTAMPS*(i+1):
            msg = ("ERROR: Unexpected error with interrupts; expected overflows: %d; received overflows: %d")%((NUM_OF_TSTAMPS*(i+1)),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)

        # calculate statistics
        avg = sum(all_measurs, 0.0) / len(all_measurs)
        all_avg.append(avg)
        print "samples          : %d"%(len(all_measurs))
        print "average          : %.2f ps"%(avg)
        print "span             : %.2f ps"%((max(all_measurs))-(min(all_measurs)))
        print "max iter %d %.2f ps"%(all_measurs.index(max(all_measurs)), max(all_measurs))
        print "min iter %d %.2f ps"%(all_measurs.index(min(all_measurs)),min(all_measurs))
        print "-----------------------------------------------------------------\n\n"


    #------------> swap of cables 
    # Standard in/out temporary redirection
    tmp_stdout = sys.stdout;
    sys.stdout = sys.__stdout__;
    tmp_stdin = sys.stdin;
    sys.stdin = sys.__stdin__;

    ask = raw_input("--> Please swap cables; press y/n when finished [y/n]: ")
    ask = ask.upper()
    if (ask == "N"):
        msg = ("ERROR: Operator did not manage to swap the cables")
        print (msg)
        raise PtsError (msg)
    else:
         msg = ("Thank you for the swap! Test continuing..")
         print (msg)

    # Standard in/out redirection
    sys.stdout = tmp_stdout;
    sys.stdin  = tmp_stdin;

    #------------> loop with cables swapped
    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               = []

        print "Ch1 - Ch%d cables swapped"%(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 != NUM_OF_TSTAMPS*(i+5):
            msg = ("ERROR: Unexpected error with interrupts; expected overflows: %d; received overflows: %d")%((NUM_OF_TSTAMPS*(i+5)),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)

        # calculate statistics
        avg = sum(all_measurs, 0.0) / len(all_measurs)
        all_avg_cables_swap.append(avg)
        print "samples          : %d"%(len(all_measurs))
        print "average          : %.2f ps"%(avg)
        print "span             : %.2f ps"%((max(all_measurs))-(min(all_measurs)))
        print "max iter %d %.2f ps"%(all_measurs.index(max(all_measurs)), max(all_measurs))
        print "min iter %d %.2f ps"%(all_measurs.index(min(all_measurs)),min(all_measurs))
        print "-----------------------------------------------------------------\n\n"


    #------------> calibration values calculation
    calibr_list = []

    for k in range(0,len(all_avg_cables_swap),1):
        calibr     = HCCFFIC_CR000001_CALIB[k] - (float((all_avg[k] + all_avg_cables_swap[k])/2)) # calibration value in ps
        calibr_str = "%.2f"%calibr
        calibr_list.append(calibr_str)
        print ("Ch1-Ch%d: %.2f ps")%(k+2, calibr)

        if (calibr) > 1000 or (calibr) < -1000:
            msg +=("ERROR: Ch1-Ch%d calibration value %.2f ps out of accepted limits [-1000 ps .. 1000 ps]\n")%(k+2, calibr)
            print (msg)
            raise PtsError (msg)

    print calibr_list
    #------------> calibration values storage to USB EEPROM
    try:
        box_eeprom = CCP210x_Eeprom("%X"%BOX_USB_VENDOR_ID, "%X"%BOX_USB_DEVICE_ID)

        calibr_data = {}
        i = 0
        for channel in CALIBR_CHANNELS:
            calibr_data[channel] = calibr_list[i]
            i =i+1
        print calibr_data

        # Write calibration data to cp210x EEPROM
        box_eeprom.set_calibr_data(calibr_data)

        print "Readback calibration data from cp210x EEPROM."
        calibr_data = box_eeprom.get_calibr_data()
        for channel, value in calibr_data.iteritems():
            print "%5s calibration value is: %s ps" % (channel, value)

    except(CP210xEepromOperationError) as e:
        raise PtsError("Test failed: %s" % e)

    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()
