Esempio n. 1
0
class VLC(object):
    @timer_dec
    def __init__(self):
        """Constructor"""

        self.DEBUG = Global.DEBUG["VLC"] or Global.DEBUG["all"]

        self.PLOT = Global.PLOT["VLC"] or Global.PLOT["all"]

        # Indicates if using IM/DD or not
        self.IM_DD = None

        self.start_timer = timer()

        if self.DEBUG:
            print('Running VLC...')
            pass

        ###########################################################################
        # >>>>>>>>>> CREATE SIMULATION SYNC OBJECT

        # Create object for SimulationSync, used for overral simulation control and debug.
        self.sync_obj = SimulationSync(DEBUG=Global.DEBUG,
                                       PLOT=Global.PLOT,
                                       previous="VLC")

        ###########################################################################
        # >>>>>>>>>> CLEAN LOG FILE

        try:
            os.remove(Global.log_results)
        except:
            pass

        ###########################################################################
        # >>>>>>>>>> CREATE MESSAGE OBJECT

        # Creates message object.
        self.message_obj = Message(
            # input_info=Global.input_info, ### this should be now inside 'seq_config'
            n_frames=
            1,  ## Use number of frames == 1. Should transmit only one payload (per burst)
            # n_frames=len(Global.input_info["data"]),
            seq_config=Global.
            burst_config,  ### NEW: has all the information regarding the TX sequence configuration (what is being transmitted, and when)
            sync_obj=self.sync_obj)

        if self.DEBUG:
            print(self.message_obj.getInputInfo())
            pass

        ###########################################################################
        # >>>>>>>>>> CONVERT MESSAGES TO LIST OF BITSTREAMS

        # # Converts it from its original type to a stream of bits (stored in bitstream_frames)
        # self.message_obj.convertsToBitstream() ---- This is now called from within 'decodeBurstSequence'

        # Decode the input sequence for Tx.
        self.decoded_sequence = self.message_obj.decodeBurstSequence()
        # printDebug(self.decoded_sequence)

        ###########################################################################
        # >>>>>>>>>> FOR EACH MESSAGE, ITERATE THROUGH ITS BITSTREAM FROM TRANSMITTER UP TO RECEIVER

        # APPLY MULTIPROCESS ON THESE LOOP...
        # For loop for each frame bitstream (each information to be sent in Global.input_info)
        # for curr_frame in self.message_obj.getBitstreamFrames():

        ### TODO -- Remove the various 'frames'. At this level, we should only care about one tx burst.

        # for curr_frame in self.message_obj.getBitstreamFrames():
        for seq_idx in self.decoded_sequence['seq_idx']:

            # 'seq_idx' selects all correct config for current TX symbol:
            # modulation and mapping types, symbol duration

            printDebug(self.decoded_sequence['seq'][seq_idx])

            # Start number of packets
            self.sync_obj.appendToMessageDict("packets", 0)

            ###########################################################################
            # >>>>>>>>>> FOR THAT BITSTREAM, STARTS MODULATION, GIVEN MODULATION CONFIG

            # Modulator object.
            self.modulator_obj = Modulator(
                is_sync=self.decoded_sequence['seq_sync'][seq_idx],
                bitstream_frame=self.decoded_sequence['seq_data'][seq_idx],
                modulation_config=Global.modulation_config[
                    self.decoded_sequence['seq_mod'][seq_idx]],
                mapping_config=Global.mapping_config[
                    self.decoded_sequence['seq_map'][seq_idx]],
                mapping_pilot_config=Global.mapping_config[
                    self.decoded_sequence['seq_pilot_map'][seq_idx]],
                symbol_duration=self.decoded_sequence['seq_duration'][seq_idx],
                sync_obj=self.sync_obj)

            ###########################################################################
            # >>>>>>>>>> CREATES MODULATION OBJECT, DEPENDING ON THE TYPE (EX: OFDM)

            # Creates the modulator object, depending on the 'modulation_type' in 'modulation_config'
            self.modulator_obj.createModulator()

            ###########################################################################
            # >>>>>>>>>> APPLY MODULATION GIVEN CHOOSEN TYPE (EX: OFDM)

            # Applies the modulation, with modulation object just created
            self.modulator_obj.applyModulation(
                decoded_sequence=self.decoded_sequence,  ## get whole sequence
                seq_idx=seq_idx,  ## and at which index we are now
            )

            ###########################################################################
            # >>>>>>>>>> GET THE LIST OF DATA TO BE TRANSMITTED, AFTER MODULATION
            # >>>>>>>>>> DEPENDING ON TRANSMITTER THROUGHPUT, MORE THAN ONE TX_DATA
            # >>>>>>>>>> IS NEEDED

            # Return a list of symbols to be transmitted.
            # This list is defined by the throughput of the modulator
            self.tx_data_list = self.modulator_obj.getTxDataList()

            # Add sample frequency do decode seq
            self.decoded_sequence['seq_sample_freq'].append(
                self.modulator_obj.getSampleFrequency())

            ###########################################################################
            # >>>>>>>>>> STARTS TRANSMITTER, GIVEN CONFIG

            # Transmitter object.
            self.transmitter_obj = Transmitter(
                transmitter_config=Global.transmitter_config,
                tx_data_list=self.tx_data_list,
                sync_obj=self.sync_obj)

            ###########################################################################
            # >>>>>>>>>> APPLY DAC ON TX_DATA, CONVERTING TO ANALOG. CAN BE
            # >>>>>>>>>> BYPASSED BY Global.bypass_dict["DAC"]

            # Get modulation config, to check for further required actions on DAC
            modulation_config = Global.modulation_config[
                self.decoded_sequence['seq_mod'][seq_idx]]
            # default values before checking
            offset_value = 0

            # check if intensity modulation / direct detection
            if self.IM_DD is None:
                self.IM_DD = modulation_config["IM_DD"]
            elif self.IM_DD != modulation_config["IM_DD"]:
                raise ValueError(
                    f"\n\n***Error --> Not allowed to use modulation configs with both IM_DD < {self.IM_DD} > and < {modulation_config['IM_DD']} >. Choose one!\n"
                )

            try:
                OFDM_type = next(iter(modulation_config["ofdm_type"].keys()))
                if OFDM_type == "DCO-OFDM":
                    # get DCO-OFDM DC value
                    offset_value = modulation_config["ofdm_type"][OFDM_type][0]
            except:
                pass

            # Apply additional bias to LED if on IM/DD mode
            if self.IM_DD:
                offset_value += Global.tx_voltage_bias_add

            # Applies DAC:
            # Optional: Offset value will be applied AFTER DAC conversion, in analog domain
            # Optional: If IM_DD, make sure only non-negative values
            self.transmitter_obj.applyDAC(
                offset_value=offset_value,
                IM_DD=self.IM_DD,
                time_interval=self.decoded_sequence['seq_duration'][seq_idx])

            ## TODO ---- MUST APPLY THE RECONSTRUCTION FILTER (if using zero-holder)

            # # Applies Low-Pass Filter
            # self.transmitter_obj.applyFilter(
            #     filter_order = 20,
            #     # cuttof = 400e6,
            #     # cuttof = Global.simul_frequency*(0.3),
            #     cuttof = Global.simul_frequency*(0.49),
            #     filter_type = 'low'
            # )

            ###########################################################################
            # >>>>>>>>>> CALCULATES OPTICAL POWER, DEPENDING ON THE LIGHTSOURCES
            # >>>>>>>>>> OR CAN BE BYPASSED BY Global.bypass_dict["LightSource"]

            # Calculates the optical power provided by the light sources
            self.transmitter_obj.calculatesOpticalPower()

            # Gets the list of optical powers to be transmitted
            tx_data_list = self.transmitter_obj.getTxOpticalOutList()

            # Get tx data list assembled into a single tx_wave
            tx_wave, tx_time = lib.assembleWaveListSameInterval(
                signal_list=tx_data_list,
                time_interval=self.decoded_sequence['seq_duration'][seq_idx],
                time_step=Global.time_step)

            # Store tx wave info for latter sync attempt at receiver end
            self.decoded_sequence['tx_wave_list'].append(tx_wave)
            self.decoded_sequence['tx_time_list'].append(tx_time)
            self.decoded_sequence['tx_num_symbols'].append(len(tx_data_list))

            # Store end time for tx wave for latter sync attempt at receiver end
            if self.decoded_sequence['seq_end_time'] != []:
                self.decoded_sequence['seq_end_time'].append(
                    len(tx_data_list)*self.decoded_sequence['seq_duration'][seq_idx] + \
                        self.decoded_sequence['seq_end_time'][-1]
                )
            else:
                self.decoded_sequence['seq_end_time'].append(
                    len(tx_data_list) *
                    self.decoded_sequence['seq_duration'][seq_idx])

            # Store start time for tx wave for latter sync attempt at receiver end
            if self.decoded_sequence['seq_start_time'] != []:
                self.decoded_sequence['seq_start_time'].append(
                    self.decoded_sequence['seq_end_time'][-2])
            else:
                self.decoded_sequence['seq_start_time'].append(0)

            # printDebug(tx_wave)
            # plotDebug(tx_wave, tx_time, symbols='ro-')
            # printDebug()

        # Get tx data list assembled into a single tx_wave
        burst_tx_wave, burst_tx_time = lib.assembleWaveListDifferentIntervals(
            signal_list=self.decoded_sequence['tx_wave_list'],
            time_interval_list=self.decoded_sequence['seq_duration'],
            num_symbols=self.decoded_sequence['tx_num_symbols'],
            time_step=Global.time_step)

        # plotDebug(burst_tx_wave, burst_tx_time, symbols='b-')
        # printDebug()

        ###########################################################################
        # >>>>>>>>>> RETRIEVE TX_DATA LIST FOR THE CHANNEL

        # # Creates global full time vector with Global.time_frame * (number of symbols in current frame)
        # Global.full_time_vector = [np.arange(0, Global.number_of_points)*Global.time_step \
        #     + idx*Global.time_frame \
        #         for idx in range(len(tx_data_list))]

        tx_data_list = list(burst_tx_wave)  ## TODO --- NEEDED?

        self.PLOT = True

        if self.PLOT:
            handle = plt.figure(figsize=(8, 2))
            # lib.plotTxRxDataList(tx_data_list, Global.full_time_vector, 'TX DATA', handle, self.sync_obj, show = False)
            # printDebug(Global.full_time_vector)
            ################## lib.plotTxRxDataList(tx_data_list, Global.full_time_vector, 'TX DATA', handle, self.sync_obj, show = False)
            plotDebug(burst_tx_wave,
                      burst_tx_time,
                      symbols='b-',
                      label="TX DATA",
                      hold=True)

        ###########################################################################
        # >>>>>>>>>> CREATES CHANNEL GIVEN INPUT TX_DATA LIST

        # Channel object
        self.channel_obj = Channel(tx_data=burst_tx_wave,
                                   tx_data_time=burst_tx_time,
                                   time_step=Global.time_step,
                                   IM_DD=self.IM_DD,
                                   channel_SNR=Global.rx_SNR_dB,
                                   sync_obj=self.sync_obj)

        # If not bypassing Channel, calculates the channel impulse response (CIR)
        if not Global.bypass_dict["Channel"]:

            ###########################################################################
            # >>>>>>>>>> CALCULATES CHANNEL RESPONSE FOR EACH LIGHTSOURCE, IF NOT BYPASSED

            raise ValueError(
                f"\n\n***Error --> Calculation of channel response for each LightSource, when NOT bypassing 'Channel', not implemented yet!\n"
            )

            # calculates the impulse response
            self.channel_obj.calculatesChannelResponse()

        else:

            ###########################################################################
            # >>>>>>>>>> IF BYPASSING (Global.bypass_dict["Channel"]) MUST SET THE
            # >>>>>>>>>> CIR FOR EACH LIGHTSOURCE

            # sets the channel response for each lamp
            # If lightsource is not bypassed
            if not Global.bypass_dict["LightSource"]:
                # setChannelResponse for each lamp
                raise ValueError(
                    f"\n\n***Error --> Set channel response for each LightSource, when bypassing 'Channel', not implemented yet!\n"
                )
                # self.channel_obj.setChannelResponse([...])

            else:
                # set fake channel impulse response from input (defined globaly here)
                ## get minimum time duration among the input symbols for better performance
                self.channel_obj.definesChannelResponse(
                    channel_list=Global.list_of_channel_response,
                    time_duration=np.min(
                        self.decoded_sequence['seq_duration']))
                # # Set single channel response (list of 1 position) ....
                # self.channel_obj.setChannelResponse(Global.list_of_channel_response)

        ###########################################################################
        # >>>>>>>>>> APPLY EACH CIR (FOR EACH LIGHTSOURCE) TO EACH TX_DATA.

        # After channel reponse set, apply it to each lamp. Do with time domain "convolution" or in "frequency domain"
        self.channel_obj.applyChannelResponse(do_convolution=True)
        # self.channel_obj.applyChannelResponse(do_convolution = False)

        ###########################################################################
        # >>>>>>>>>> GET RX_DATA LIST CONVOLVED BY CHANNEL AFTER ADDING NOISE.

        # Gets the list of optical powers at the receiver, after convolution on channel response, and noise addition.
        rx_data_list = self.channel_obj.getRxDataOut()
        rx_time = self.channel_obj.getRxTime()  ## old Global.full_time_vector

        # # Re-calculates full time vector, after convolution
        # # Creates global full time vector with Global.time_frame * (number of symbols in current frame)
        # Global.full_time_vector = [np.arange(0, len(rx_data_list[idx]))*Global.time_step \
        #     + idx*Global.time_frame \
        #         for idx in range(len(rx_data_list))]

        if self.PLOT:
            # printDebug(rx_data_list)
            # plotDebug(tx_data_list[0], Global.full_time_vector[0], symbols='ro-')
            # plotDebug(tx_data_list[1], Global.full_time_vector[1], symbols='ro-')
            # plotDebug(rx_data_list[0], Global.full_time_vector[0], symbols='ro-')
            # plotDebug(rx_data_list[1], Global.full_time_vector[1], symbols='ro-')
            plotDebug(rx_data_list[0],
                      rx_time,
                      symbols='r-',
                      label="RX DATA",
                      hold=False)

            # lib.plotTxRxDataList(rx_data_list, 'RX DATA', handle, self.sync_obj, show = True)

        # printDebug(rx_data_list)
        # plotDebug(rx_data_list[0], rx_time)

        ## TODO --- NEW FOR HERE?... CHECK HOW TO DECODE DATA...TRY TO DECODE FIELD BY FIELD?
        # for seq_idx in self.decoded_sequence['seq_idx']:

        ## TODO --- temp fix: rx is a list now

        # Reset IM/DD variable. Resets before the 'for', b/c it should be the same for all PDs below.
        self.IM_DD = None

        # Get the sample frequency defined so far
        sample_frequency = self.modulator_obj.getSampleFrequency()

        # Each tx [led] convolves with channel [pd x led], producing rx [pd].
        # get the rx_data wave for each receiver.
        for rx_data_pd in rx_data_list:

            # # printDebug(rx_data_pd)
            # plotDebug(rx_data_pd, rx_time)
            # plotDebug(rx_data_pd, rx_time, symbols="bo-")

            # Start the index to know where on the sequence we are, for each rx_data.
            # The trick here is that the for each rx_data, we have all phases of the modulation sequence.
            # And since they may come from different channel responses, delays may be different, etc.
            # So, should try to decode the sequence for EACH rx_data_pd.
            # The 'seq_idx' is the ID that controls where in the sequence we are, and should go from 0 to max
            # during this 'for' loop. As long as it progresses, we increment 'seq_idx', and reset when loops back.
            seq_idx = 0

            ###########################################################################
            # >>>>>>>>>> STARTS RECEIVER, GIVEN CONFIG
            # Receiver object.
            self.receiver_obj = Receiver(
                receiver_config=Global.receiver_config,
                roic_config=Global.
                roic_config,  # read-out integrated circuit config for that rx
                rx_data=rx_data_pd,
                rx_time=rx_time,
                sample_freq=sample_frequency,
                sample_phase=0,  ## TODO -- MUST BE APPLIED
                sync_obj=self.sync_obj)

            # while(seq_idx != MAX???):

            # lib.plotBode(rx_data_list[0], filtered, time_frame, number_samples, cuttof)

            ###########################################################################
            # >>>>>>>>>> CALCULATES PHOTOCURRENTS, DEPENDING ON THE DETECTORS

            # Calculates the photocurrents, provided by the detectors
            # This provides an 'analog' voltage, with time equal to 2*Global.number_of_points
            # This is because of the channel convolution, that will expand the signal due to delays
            self.receiver_obj.calculatesPhotocurrent()

            ###########################################################################
            # >>>>>>>>>> CALCULATES THE OUTPUT VOLTAGE

            # Calculates the output voltage.
            # Here is where the current signal is sampled on the input clock sample frequency
            self.receiver_obj.calculatesOutVoltage()

            ###########################################################################
            # >>>>>>>>>> APPLY ADC ON RX_DATA, CONVERTING FROM ANALOG TO DIGITAL

            ## TODO ---- ADD HERE THE REMOVAL OF THE DC VALUE OF DCO-OFDM DEMODULATION  (VOLTAGE DOMAIN)

            # Until now, the 'seq_idx' index wasn't anaylsed, since we are converting from optical to voltage domain.
            # Now, we need info to know what kind of modulation is the first one, so we know if have to add any DC bias.
            # If DCO-OFDM, then all other parts of the sequence should have the same OFDM type, and getting first position is
            # enough to know we need to add DC for whole wave.

            # Get modulation config
            modulation_config = Global.modulation_config[
                self.decoded_sequence['seq_mod'][seq_idx]]

            # default values before checking
            offset_value = 0

            # check if intensity modulation / direct detection
            if self.IM_DD is None:
                self.IM_DD = modulation_config["IM_DD"]
            try:
                OFDM_type = next(iter(modulation_config["ofdm_type"].keys()))
                if OFDM_type == "DCO-OFDM":
                    # Get DCO-OFDM DC value
                    offset_value = modulation_config["ofdm_type"][OFDM_type][0]
            except:
                pass

            # Subtract bias to voltege from the ROIC, if on IM/DD mode
            if self.IM_DD:
                offset_value -= Global.rx_voltage_bias_subtract

            # Applies ADC:
            # Passes sample frequency used on Modulator (TODO -- CHECK THIS VALUE FOR EACH)
            self.receiver_obj.applyADC(
                # sample_freq = self.modulator_obj.getSampleFrequency(),
                offset_value=offset_value
                # IM_DD = self.IM_DD
                # time_interval = self.decoded_sequence['seq_duration'][seq_idx],
            )

            # self.receiver_obj.applyADC(sample_freq = Global.N_FFT/Global.time_frame)

            # plotDebug(self.receiver_obj.adc_rx_data_list, symbols='ro-')
            # old = self.receiver_obj.adc_rx_data_list

            # # After sampling, apply digital filter
            # # Applies Low-Pass Filter
            # self.receiver_obj.applyFilter(
            #     filter_order = 20,
            #     # cuttof = 400e6,
            #     # cuttof = Global.simul_frequency*(0.3),
            #     # cuttof = Global.simul_frequency*(0.49),
            #     cuttof = 1e4,
            #     # cuttof = (1/Global.time_frame)*(0.5),
            #     filter_type = 'hp'
            # )

            # plotDebug(self.receiver_obj.adc_rx_data_list, symbols='ro-')
            # plotDebug(self.receiver_obj.adc_rx_data_list - old, symbols='ro-')
            # # Applies High-Pass Filter
            # self.receiver_obj.applyFilter(
            #     filter_order = 20,
            #     # cuttof = 400e6,
            #     # cuttof = Global.simul_frequency*(0.3),
            #     cuttof = 1/Global.time_frame,
            #     filter_type = 'hp'
            # )

            # plotDebug(self.receiver_obj.getAdcRxDataList().imag)

            # Starts the rx_data as NOT-synced
            self.rx_data_synced = False

            # Start delay steps and time variables
            self.delay_time = 0
            self.delay_steps = 0

            printDebug(self.decoded_sequence)
            # From here, we have the full wave, already sampled. Now, need to debug.
            # 'seq_idx' starts with 0. Then increments from here, as long as we progress on the sequence.
            # for seq_idx in range(0, len(self.decoded_sequence['seq_data'])):
            while seq_idx < len(self.decoded_sequence['seq']):
                # Current Sequence ID
                print(
                    f"**********************************************************************************\n\
Starting sequence for field < {self.decoded_sequence['seq'][seq_idx].split('.')[0]} > and subfield < {self.decoded_sequence['seq'][seq_idx].split('.')[1]} >\n\
**********************************************************************************\n"
                )

                # Re-create Modulator object, for the DeModulation, for each sequence step.
                self.modulator_obj = Modulator(
                    is_sync=self.decoded_sequence['seq_sync'][seq_idx],
                    bitstream_frame=self.decoded_sequence['seq_data'][seq_idx],
                    modulation_config=Global.modulation_config[
                        self.decoded_sequence['seq_mod'][seq_idx]],
                    mapping_config=Global.mapping_config[
                        self.decoded_sequence['seq_map'][seq_idx]],
                    mapping_pilot_config=Global.mapping_config[
                        self.decoded_sequence['seq_pilot_map'][seq_idx]],
                    symbol_duration=self.decoded_sequence['seq_duration']
                    [seq_idx],
                    sync_obj=self.sync_obj)

                # Re-create the modulator.
                self.modulator_obj.createModulator()

                # Re-set the sample frequency, for new modulator object.
                self.modulator_obj.setSampleFrequency(sample_frequency)

                ###########################################################################
                # >>>>>>>>>> GET ADC RX_DATA TO PASS FOR DE-MODULATOR

                # Set the rx_data and rx_time to modulator.
                self.modulator_obj.setRxData(
                    self.receiver_obj.getSampledWave())
                self.modulator_obj.setRxTime(
                    self.receiver_obj.getSampledWaveTime())

                # plotDebug(self.receiver_obj.getSampledWave(), self.receiver_obj.getSampledWaveTime(), symbols='ro-')

                # # # TODO ----- check if can del receiver object already...
                # del self.receiver_obj

                # # TODO ----- FROM HERE, WE HAVE THE WHOLE SAMPLED WAVE FOR A GIVEN RX. NEED TO DECODE THE WHOLE SEQUENCE NOW.

                ###########################################################################
                # >>>>>>>>>> SET THE ACTUAL CHANNEL RESPONSE, FOR FURTHER COMPARISSONS

                # Sets the list of channel responses, for further comparissons with estimated ones
                self.modulator_obj.setListOfChannelResponses(
                    self.channel_obj.getChannelResponse())

                ###########################################################################
                # >>>>>>>>>> APPLY DE-MODULATION GIVEN TYPE CHOOSEN BEFORE (EX: OFDM)

                # Check how to do the demodulation, first on the remove group delay (need to use/check for the FLP method...)

                # Applies the modulation, with modulation object just created.
                # If on the 'sync' step, apply the synchronization first.
                self.rx_data_synced, self.delay_time, self.delay_steps = self.modulator_obj.applyDeModulation(
                    decoded_sequence=self.
                    decoded_sequence,  ## get whole sequence
                    seq_idx=seq_idx,  ## and at which index we are now
                    rx_data_synced=self.
                    rx_data_synced,  ## get if data is sync or not
                    delay_time=self.delay_time,  ## get delay in time
                    delay_steps=self.delay_steps  ## get delay in steps
                )

                printDebug(self.delay_time)
                # Check if current sequence step is for 'sync' or for data.
                # if self.decoded_sequence['rx_data'][seq_idx] != 'sync':
                if 'sync' in self.decoded_sequence['rx_data'][seq_idx]:

                    separators = ''.join(30 * ['##'])

                    pretty_diff = f"""{separators}
ID for current sequence step: < {self.decoded_sequence['seq'][seq_idx]} >
Sync data, with found delay of:
    {self.delay_time} seconds
    or
    {self.delay_time*1e6} us
    or
    {self.delay_time*1e9} ns

"""
                    # Write to log file
                    with open(Global.log_results, "a") as results:
                        results.write(pretty_diff)

                elif 'sync' not in self.decoded_sequence['rx_data'][seq_idx]:

                    ###########################################################################
                    # >>>>>>>>>> RETRIEVE RX DATA

                    # Get the received frame message
                    curr_rx_frame = self.modulator_obj.getRxBitstreamFrame()

                    # printDebug(curr_rx_frame)

                    # handle = plt.figure(figsize=(8,2))
                    # lib.plotTxRxDataList(curr_rx_frame, 'TEST', handle, self.sync_obj, show = True)

                    # if self.DEBUG:
                    #     print(f'curr_rx_frame = {curr_rx_frame}')
                    #     pass

                    # Append current rx frame
                    # rx_frames.append(curr_rx_frame)

                    # pp(self.receiver_obj)

                    ###########################################################################
                    # >>>>>>>>>> SET RECOVERED BITSTREAM TO MESSAGE OBJECT

                    self.message_obj.setRxBitstreamFrames(curr_rx_frame)

                    ###########################################################################
                    # >>>>>>>>>> CREATE MERIT FUNCTION OBJECT

                    # Merit Funcions object
                    self.merit_functions_obj = MeritFunctions(
                        sync_obj=self.sync_obj)

                    ###########################################################################
                    # >>>>>>>>>> GET < BER > FOR EACH FRAME
                    print()
                    print()
                    print()

                    # printDebug(self.decoded_sequence['seq_data'][seq_idx])
                    # printDebug(self.message_obj.getRxBitstreamFrames())

                    # print BER
                    self.BER, self.NBER, self.pretty_diff = self.merit_functions_obj.calculateBER(
                        self.decoded_sequence['seq_data'][seq_idx],
                        self.message_obj.getRxBitstreamFrames())

                    self.sync_obj.appendToMessageDict("BER", self.BER)
                    self.sync_obj.appendToMessageDict("NBER", self.NBER)

                    ###########################################################################
                    # >>>>>>>>>> SHOW RX AND TX VALUES

                    binary_rx_data = self.message_obj.BitstreamToMessage(
                        tx_data=self.decoded_sequence['seq_data'][seq_idx],
                        rx_data=curr_rx_frame,
                        bitstream_type=self.decoded_sequence['seq_data_type']
                        [seq_idx])

                    if self.DEBUG:
                        pass

                    self.message_obj.compareMessages(
                        seq_id=self.decoded_sequence['seq'][seq_idx],
                        tx_data=self.decoded_sequence['seq_data'][seq_idx],
                        rx_data=binary_rx_data,
                        pretty_diff=self.pretty_diff)

                    # Add input and received info to message dictionary
                    self.sync_obj.appendToMessageDict(
                        "tx_info", self.decoded_sequence['seq_data'][seq_idx])
                    self.sync_obj.appendToMessageDict("rx_info",
                                                      binary_rx_data)
                    self.sync_obj.appendToMessageDict("n_bits", \
                        [len(stream) for stream in self.message_obj.getBitstreamFrames()])

                # Goes to next step on the sequence
                seq_idx += 1

        # Write to log file the decoded_sequence
        with open(Global.log_results, "a") as results:

            json_str = ''.join(30 * ['##']) + '\n'

            for seq_step in self.decoded_sequence.keys():
                if seq_step not in [
                        "seq_all_mapped_info", "tx_time_list", "tx_wave_list",
                        "seq_func_rx", "seq_func_tx"
                ]:
                    json_str += f"## {seq_step} ##\n"
                    for data in self.decoded_sequence[seq_step]:
                        json_str += f"\t< {data} >\n"
            results.write(json_str)

        # printDebug(self.decoded_sequence['rx_data'])
        # # starts empty list with all received frames
        # rx_frames = self.decoded_sequence['rx_data']

        # self.message_obj.convertBackSequence

        # ###########################################################################
        # # >>>>>>>>>> SET RECOVERED BITSTREAM TO MESSAGE OBJECT

        # self.message_obj.setRxBitstreamFrames(rx_frames)

        # ###########################################################################
        # # >>>>>>>>>> CREATE MERIT FUNCTION OBJECT

        # # Merit Funcions object
        # self.merit_functions_obj = MeritFunctions(
        #     sync_obj = self.sync_obj
        # )

        # ###########################################################################
        # # >>>>>>>>>> GET < BER > FOR EACH FRAME
        # print()
        # print()
        # print()

        # # print BER
        # self.BER, self.NBER = self.merit_functions_obj.calculateBER(
        #     self.message_obj.getBitstreamFrames(),
        #     self.message_obj.getRxBitstreamFrames()
        # )

        # self.sync_obj.appendToMessageDict("BER", self.BER)
        # self.sync_obj.appendToMessageDict("NBER", self.NBER)

        # ###########################################################################
        # # >>>>>>>>>> SHOW RX AND TX VALUES

        # if self.DEBUG:
        #     pass

        # self.message_obj.compareMessages(
        #     Global.input_info["data"],
        #     self.message_obj.BitstreamToMessage()
        # )

        # # Add input and received info to message dictionary
        # self.sync_obj.appendToMessageDict("tx_info", Global.input_info["data"])
        # self.sync_obj.appendToMessageDict("rx_info", self.message_obj.BitstreamToMessage())
        # self.sync_obj.appendToMessageDict("n_bits", \
        #     [len(stream) for stream in self.message_obj.getBitstreamFrames()])

        ###########################################################################
        # >>>>>>>>>> PRINTS FULL SIMUL PATH, FOR DEBUG

        if self.DEBUG and False:
            # Prints full simulation path
            print(self.sync_obj.showSimulationPath())
            self.total_time = timer() - self.start_timer
            print(
                f"\nTotal execution time was << {self.total_time} >> seconds.\n"
            )

        # ,
        #     self.message_obj.getRxBitstreamFrames()
        print(self.sync_obj.showMessageDict())

    @sync_track
    def getBER(self):
        """Returns value of self.BER"""

        return self.BER

    @sync_track
    def setBER(self, BER):
        """Set new value for self.BER"""

        self.BER = BER

    @sync_track
    def getNBER(self):
        """Returns value of self.NBER"""

        return self.NBER

    @sync_track
    def setNBER(self, NBER):
        """Set new value for self.NBER"""

        self.NBER = NBER

    @sync_track
    def getMessageObj(self):
        """Returns value of self.message_obj"""

        return self.message_obj

    @sync_track
    def setMessageObj(self, message_obj):
        """Set new value for self.message_obj"""

        self.message_obj = message_obj

    @sync_track
    def getMappingObj(self):
        """Returns value of self.mapping_obj"""

        return self.mapping_obj

    @sync_track
    def setMappingObj(self, mapping_obj):
        """Set new value for self.mapping_obj"""

        self.mapping_obj = mapping_obj

    @sync_track
    def getModulatorObj(self):
        """Returns value of self.modulator_obj"""

        return self.modulator_obj

    @sync_track
    def setModulatorObj(self, modulator_obj):
        """Set new value for self.modulator_obj"""

        self.modulator_obj = modulator_obj

    @sync_track
    def getTransmitterObj(self):
        """Returns value of self.transmitter_obj"""

        return self.transmitter_obj

    @sync_track
    def setTransmitterObj(self, transmitter_obj):
        """Set new value for self.transmitter_obj"""

        self.transmitter_obj = transmitter_obj

    @sync_track
    def getChannelObj(self):
        """Returns value of self.channel_obj"""

        return self.channel_obj

    @sync_track
    def setChannelObj(self, channel_obj):
        """Set new value for self.channel_obj"""

        self.channel_obj = channel_obj

    @sync_track
    def getReceiverObj(self):
        """Returns value of self.receiver_obj"""

        return self.receiver_obj

    @sync_track
    def setReceiverObj(self, receiver_obj):
        """Set new value for self.receiver_obj"""

        self.receiver_obj = receiver_obj

    @sync_track
    def getMeritFunctionsObj(self):
        """Returns value of self.merit_functions_obj"""

        return self.merit_functions_obj

    @sync_track
    def setMeritFunctionsObj(self, merit_functions_obj):
        """Set new value for self.merit_functions_obj"""

        self.merit_functions_obj = merit_functions_obj