1#!/usr/bin/env python3
2#
3#   Copyright 2018 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the 'License');
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an 'AS IS' BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import time
18from enum import Enum
19
20import numpy as np
21from acts.controllers import cellular_simulator
22from acts.test_utils.tel.tel_test_utils import get_telephony_signal_strength
23from acts.test_utils.tel.tel_test_utils import toggle_airplane_mode
24from acts.test_utils.tel.tel_test_utils import toggle_cell_data_roaming
25from acts.test_utils.tel.tel_test_utils import get_rx_tx_power_levels
26
27
28class BaseSimulation():
29    """ Base class for cellular connectivity simulations.
30
31    Classes that inherit from this base class implement different simulation
32    setups. The base class contains methods that are common to all simulation
33    configurations.
34
35    """
36
37    NUM_UL_CAL_READS = 3
38    NUM_DL_CAL_READS = 5
39    MAX_BTS_INPUT_POWER = 30
40    MAX_PHONE_OUTPUT_POWER = 23
41    UL_MIN_POWER = -60.0
42
43    # Keys to obtain settings from the test_config dictionary.
44    KEY_CALIBRATION = "calibration"
45    KEY_ATTACH_RETRIES = "attach_retries"
46    KEY_ATTACH_TIMEOUT = "attach_timeout"
47
48    # Filepath to the config files stored in the Anritsu callbox. Needs to be
49    # formatted to replace {} with either A or B depending on the model.
50    CALLBOX_PATH_FORMAT_STR = 'C:\\Users\\MD8475{}\\Documents\\DAN_configs\\'
51
52    # Time in seconds to wait for the phone to settle
53    # after attaching to the base station.
54    SETTLING_TIME = 10
55
56    # Default time in seconds to wait for the phone to attach to the basestation
57    # after toggling airplane mode. This setting can be changed with the
58    # KEY_ATTACH_TIMEOUT keyword in the test configuration file.
59    DEFAULT_ATTACH_TIMEOUT = 120
60
61    # The default number of attach retries. This setting can be changed with
62    # the KEY_ATTACH_RETRIES keyword in the test configuration file.
63    DEFAULT_ATTACH_RETRIES = 3
64
65    # These two dictionaries allow to map from a string to a signal level and
66    # have to be overriden by the simulations inheriting from this class.
67    UPLINK_SIGNAL_LEVEL_DICTIONARY = {}
68    DOWNLINK_SIGNAL_LEVEL_DICTIONARY = {}
69
70    # Units for downlink signal level. This variable has to be overriden by
71    # the simulations inheriting from this class.
72    DOWNLINK_SIGNAL_LEVEL_UNITS = None
73
74    class BtsConfig:
75        """ Base station configuration class. This class is only a container for
76        base station parameters and should not interact with the instrument
77        controller.
78
79        Atributes:
80            output_power: a float indicating the required signal level at the
81                instrument's output.
82            input_level: a float indicating the required signal level at the
83                instrument's input.
84        """
85        def __init__(self):
86            """ Initialize the base station config by setting all its
87            parameters to None. """
88            self.output_power = None
89            self.input_power = None
90            self.band = None
91
92        def incorporate(self, new_config):
93            """ Incorporates a different configuration by replacing the current
94            values with the new ones for all the parameters different to None.
95            """
96            for attr, value in vars(new_config).items():
97                if value:
98                    setattr(self, attr, value)
99
100    def __init__(self, simulator, log, dut, test_config, calibration_table):
101        """ Initializes the Simulation object.
102
103        Keeps a reference to the callbox, log and dut handlers and
104        initializes the class attributes.
105
106        Args:
107            simulator: a cellular simulator controller
108            log: a logger handle
109            dut: the android device handler
110            test_config: test configuration obtained from the config file
111            calibration_table: a dictionary containing path losses for
112                different bands.
113        """
114
115        self.simulator = simulator
116        self.log = log
117        self.dut = dut
118        self.calibration_table = calibration_table
119
120        # Turn calibration on or off depending on the test config value. If the
121        # key is not present, set to False by default
122        if self.KEY_CALIBRATION not in test_config:
123            self.log.warning('The {} key is not set in the testbed '
124                             'parameters. Setting to off by default. To '
125                             'turn calibration on, include the key with '
126                             'a true/false value.'.format(
127                                 self.KEY_CALIBRATION))
128
129        self.calibration_required = test_config.get(self.KEY_CALIBRATION,
130                                                    False)
131
132        # Obtain the allowed number of retries from the test configs
133        if self.KEY_ATTACH_RETRIES not in test_config:
134            self.log.warning('The {} key is not set in the testbed '
135                             'parameters. Setting to {} by default.'.format(
136                                 self.KEY_ATTACH_RETRIES,
137                                 self.DEFAULT_ATTACH_RETRIES))
138
139        self.attach_retries = test_config.get(self.KEY_ATTACH_RETRIES,
140                                              self.DEFAULT_ATTACH_RETRIES)
141
142        # Obtain the attach timeout from the test configs
143        if self.KEY_ATTACH_TIMEOUT not in test_config:
144            self.log.warning('The {} key is not set in the testbed '
145                             'parameters. Setting to {} by default.'.format(
146                                 self.KEY_ATTACH_TIMEOUT,
147                                 self.DEFAULT_ATTACH_TIMEOUT))
148
149        self.attach_timeout = test_config.get(self.KEY_ATTACH_TIMEOUT,
150                                              self.DEFAULT_ATTACH_TIMEOUT)
151
152        # Configuration object for the primary base station
153        self.primary_config = self.BtsConfig()
154
155        # Store the current calibrated band
156        self.current_calibrated_band = None
157
158        # Path loss measured during calibration
159        self.dl_path_loss = None
160        self.ul_path_loss = None
161
162        # Target signal levels obtained during configuration
163        self.sim_dl_power = None
164        self.sim_ul_power = None
165
166        # Stores RRC status change timer
167        self.rrc_sc_timer = None
168
169        # Set to default APN
170        log.info("Configuring APN.")
171        dut.droid.telephonySetAPN("test", "test", "default")
172
173        # Enable roaming on the phone
174        toggle_cell_data_roaming(self.dut, True)
175
176        # Make sure airplane mode is on so the phone won't attach right away
177        toggle_airplane_mode(self.log, self.dut, True)
178
179        # Wait for airplane mode setting to propagate
180        time.sleep(2)
181
182        # Prepare the simulator for this simulation setup
183        self.setup_simulator()
184
185    def setup_simulator(self):
186        """ Do initial configuration in the simulator. """
187        raise NotImplementedError()
188
189    def attach(self):
190        """ Attach the phone to the basestation.
191
192        Sets a good signal level, toggles airplane mode
193        and waits for the phone to attach.
194
195        Returns:
196            True if the phone was able to attach, False if not.
197        """
198
199        # Turn on airplane mode
200        toggle_airplane_mode(self.log, self.dut, True)
201
202        # Wait for airplane mode setting to propagate
203        time.sleep(2)
204
205        # Provide a good signal power for the phone to attach easily
206        new_config = self.BtsConfig()
207        new_config.input_power = -10
208        new_config.output_power = -30
209        self.simulator.configure_bts(new_config)
210        self.primary_config.incorporate(new_config)
211
212        # Try to attach the phone.
213        for i in range(self.attach_retries):
214
215            try:
216
217                # Turn off airplane mode
218                toggle_airplane_mode(self.log, self.dut, False)
219
220                # Wait for the phone to attach.
221                self.simulator.wait_until_attached(timeout=self.attach_timeout)
222
223            except cellular_simulator.CellularSimulatorError:
224
225                # The phone failed to attach
226                self.log.info(
227                    "UE failed to attach on attempt number {}.".format(i + 1))
228
229                # Turn airplane mode on to prepare the phone for a retry.
230                toggle_airplane_mode(self.log, self.dut, True)
231
232                # Wait for APM to propagate
233                time.sleep(3)
234
235                # Retry
236                if i < self.attach_retries - 1:
237                    # Retry
238                    continue
239                else:
240                    # No more retries left. Return False.
241                    return False
242
243            else:
244                # The phone attached successfully.
245                time.sleep(self.SETTLING_TIME)
246                self.log.info("UE attached to the callbox.")
247                break
248
249        return True
250
251    def detach(self):
252        """ Detach the phone from the basestation.
253
254        Turns airplane mode and resets basestation.
255        """
256
257        # Set the DUT to airplane mode so it doesn't see the
258        # cellular network going off
259        toggle_airplane_mode(self.log, self.dut, True)
260
261        # Wait for APM to propagate
262        time.sleep(2)
263
264        # Power off basestation
265        self.simulator.detach()
266
267    def stop(self):
268        """  Detach phone from the basestation by stopping the simulation.
269
270        Stop the simulation and turn airplane mode on. """
271
272        # Set the DUT to airplane mode so it doesn't see the
273        # cellular network going off
274        toggle_airplane_mode(self.log, self.dut, True)
275
276        # Wait for APM to propagate
277        time.sleep(2)
278
279        # Stop the simulation
280        self.simulator.stop()
281
282    def start(self):
283        """ Start the simulation by attaching the phone and setting the
284        required DL and UL power.
285
286        Note that this refers to starting the simulated testing environment
287        and not to starting the signaling on the cellular instruments,
288        which might have been done earlier depending on the cellular
289        instrument controller implementation. """
290
291        if not self.attach():
292            raise RuntimeError('Could not attach to base station.')
293
294        # Starts IP traffic while changing this setting to force the UE to be
295        # in Communication state, as UL power cannot be set in Idle state
296        self.start_traffic_for_calibration()
297
298        # Wait until it goes to communication state
299        self.simulator.wait_until_communication_state()
300
301        # Set uplink power to a minimum before going to the actual desired
302        # value. This avoid inconsistencies produced by the hysteresis in the
303        # PA switching points.
304        self.log.info('Setting UL power to -30 dBm before going to the '
305                      'requested value to avoid incosistencies caused by '
306                      'hysteresis.')
307        self.set_uplink_tx_power(-30)
308
309        # Set signal levels obtained from the test parameters
310        self.set_downlink_rx_power(self.sim_dl_power)
311        self.set_uplink_tx_power(self.sim_ul_power)
312
313        # Verify signal level
314        try:
315            rx_power, tx_power = get_rx_tx_power_levels(self.log, self.dut)
316
317            if not tx_power or not rx_power[0]:
318                raise RuntimeError('The method return invalid Tx/Rx values.')
319
320            self.log.info('Signal level reported by the DUT in dBm: Tx = {}, '
321                          'Rx = {}.'.format(tx_power, rx_power))
322
323            if abs(self.sim_ul_power - tx_power) > 1:
324                self.log.warning('Tx power at the UE is off by more than 1 dB')
325
326        except RuntimeError as e:
327            self.log.error('Could not verify Rx / Tx levels: %s.' % e)
328
329        # Stop IP traffic after setting the UL power level
330        self.stop_traffic_for_calibration()
331
332    def parse_parameters(self, parameters):
333        """ Configures simulation using a list of parameters.
334
335        Consumes parameters from a list.
336        Children classes need to call this method first.
337
338        Args:
339            parameters: list of parameters
340        """
341
342        raise NotImplementedError()
343
344    def consume_parameter(self, parameters, parameter_name, num_values=0):
345        """ Parses a parameter from a list.
346
347        Allows to parse the parameter list. Will delete parameters from the
348        list after consuming them to ensure that they are not used twice.
349
350        Args:
351            parameters: list of parameters
352            parameter_name: keyword to look up in the list
353            num_values: number of arguments following the
354                parameter name in the list
355        Returns:
356            A list containing the parameter name and the following num_values
357            arguments
358        """
359
360        try:
361            i = parameters.index(parameter_name)
362        except ValueError:
363            # parameter_name is not set
364            return []
365
366        return_list = []
367
368        try:
369            for j in range(num_values + 1):
370                return_list.append(parameters.pop(i))
371        except IndexError:
372            raise ValueError(
373                "Parameter {} has to be followed by {} values.".format(
374                    parameter_name, num_values))
375
376        return return_list
377
378    def set_uplink_tx_power(self, signal_level):
379        """ Configure the uplink tx power level
380
381        Args:
382            signal_level: calibrated tx power in dBm
383        """
384        new_config = self.BtsConfig()
385        new_config.input_power = self.calibrated_uplink_tx_power(
386            self.primary_config, signal_level)
387        self.simulator.configure_bts(new_config)
388        self.primary_config.incorporate(new_config)
389
390    def set_downlink_rx_power(self, signal_level):
391        """ Configure the downlink rx power level
392
393        Args:
394            signal_level: calibrated rx power in dBm
395        """
396        new_config = self.BtsConfig()
397        new_config.output_power = self.calibrated_downlink_rx_power(
398            self.primary_config, signal_level)
399        self.simulator.configure_bts(new_config)
400        self.primary_config.incorporate(new_config)
401
402    def get_uplink_power_from_parameters(self, parameters):
403        """ Reads uplink power from a list of parameters. """
404
405        values = self.consume_parameter(parameters, self.PARAM_UL_PW, 1)
406
407        if values:
408            if values[1] in self.UPLINK_SIGNAL_LEVEL_DICTIONARY:
409                return self.UPLINK_SIGNAL_LEVEL_DICTIONARY[values[1]]
410            else:
411                try:
412                    if values[1][0] == 'n':
413                        # Treat the 'n' character as a negative sign
414                        return -int(values[1][1:])
415                    else:
416                        return int(values[1])
417                except ValueError:
418                    pass
419
420        # If the method got to this point it is because PARAM_UL_PW was not
421        # included in the test parameters or the provided value was invalid.
422        raise ValueError(
423            "The test name needs to include parameter {} followed by the "
424            "desired uplink power expressed by an integer number in dBm "
425            "or by one the following values: {}. To indicate negative "
426            "values, use the letter n instead of - sign.".format(
427                self.PARAM_UL_PW,
428                list(self.UPLINK_SIGNAL_LEVEL_DICTIONARY.keys())))
429
430    def get_downlink_power_from_parameters(self, parameters):
431        """ Reads downlink power from a list of parameters. """
432
433        values = self.consume_parameter(parameters, self.PARAM_DL_PW, 1)
434
435        if values:
436            if values[1] not in self.DOWNLINK_SIGNAL_LEVEL_DICTIONARY:
437                raise ValueError("Invalid signal level value {}.".format(
438                    values[1]))
439            else:
440                return self.DOWNLINK_SIGNAL_LEVEL_DICTIONARY[values[1]]
441        else:
442            # Use default value
443            power = self.DOWNLINK_SIGNAL_LEVEL_DICTIONARY['excellent']
444            self.log.info("No DL signal level value was indicated in the test "
445                          "parameters. Using default value of {} {}.".format(
446                              power, self.DOWNLINK_SIGNAL_LEVEL_UNITS))
447            return power
448
449    def calibrated_downlink_rx_power(self, bts_config, signal_level):
450        """ Calculates the power level at the instrument's output in order to
451        obtain the required rx power level at the DUT's input.
452
453        If calibration values are not available, returns the uncalibrated signal
454        level.
455
456        Args:
457            bts_config: the current configuration at the base station. derived
458                classes implementations can use this object to indicate power as
459                spectral power density or in other units.
460            signal_level: desired downlink received power, can be either a
461                key value pair, an int or a float
462        """
463
464        # Obtain power value if the provided signal_level is a key value pair
465        if isinstance(signal_level, Enum):
466            power = signal_level.value
467        else:
468            power = signal_level
469
470        # Try to use measured path loss value. If this was not set, it will
471        # throw an TypeError exception
472        try:
473            calibrated_power = round(power + self.dl_path_loss)
474            if calibrated_power > self.simulator.MAX_DL_POWER:
475                self.log.warning(
476                    "Cannot achieve phone DL Rx power of {} dBm. Requested TX "
477                    "power of {} dBm exceeds callbox limit!".format(
478                        power, calibrated_power))
479                calibrated_power = self.simulator.MAX_DL_POWER
480                self.log.warning(
481                    "Setting callbox Tx power to max possible ({} dBm)".format(
482                        calibrated_power))
483
484            self.log.info(
485                "Requested phone DL Rx power of {} dBm, setting callbox Tx "
486                "power at {} dBm".format(power, calibrated_power))
487            time.sleep(2)
488            # Power has to be a natural number so calibration wont be exact.
489            # Inform the actual received power after rounding.
490            self.log.info(
491                "Phone downlink received power is {0:.2f} dBm".format(
492                    calibrated_power - self.dl_path_loss))
493            return calibrated_power
494        except TypeError:
495            self.log.info("Phone downlink received power set to {} (link is "
496                          "uncalibrated).".format(round(power)))
497            return round(power)
498
499    def calibrated_uplink_tx_power(self, bts_config, signal_level):
500        """ Calculates the power level at the instrument's input in order to
501        obtain the required tx power level at the DUT's output.
502
503        If calibration values are not available, returns the uncalibrated signal
504        level.
505
506        Args:
507            bts_config: the current configuration at the base station. derived
508                classes implementations can use this object to indicate power as
509                spectral power density or in other units.
510            signal_level: desired uplink transmitted power, can be either a
511                key value pair, an int or a float
512        """
513
514        # Obtain power value if the provided signal_level is a key value pair
515        if isinstance(signal_level, Enum):
516            power = signal_level.value
517        else:
518            power = signal_level
519
520        # Try to use measured path loss value. If this was not set, it will
521        # throw an TypeError exception
522        try:
523            calibrated_power = round(power - self.ul_path_loss)
524            if calibrated_power < self.UL_MIN_POWER:
525                self.log.warning(
526                    "Cannot achieve phone UL Tx power of {} dBm. Requested UL "
527                    "power of {} dBm exceeds callbox limit!".format(
528                        power, calibrated_power))
529                calibrated_power = self.UL_MIN_POWER
530                self.log.warning(
531                    "Setting UL Tx power to min possible ({} dBm)".format(
532                        calibrated_power))
533
534            self.log.info(
535                "Requested phone UL Tx power of {} dBm, setting callbox Rx "
536                "power at {} dBm".format(power, calibrated_power))
537            time.sleep(2)
538            # Power has to be a natural number so calibration wont be exact.
539            # Inform the actual transmitted power after rounding.
540            self.log.info(
541                "Phone uplink transmitted power is {0:.2f} dBm".format(
542                    calibrated_power + self.ul_path_loss))
543            return calibrated_power
544        except TypeError:
545            self.log.info("Phone uplink transmitted power set to {} (link is "
546                          "uncalibrated).".format(round(power)))
547            return round(power)
548
549    def calibrate(self, band):
550        """ Calculates UL and DL path loss if it wasn't done before.
551
552        The should be already set to the required band before calling this
553        method.
554
555        Args:
556            band: the band that is currently being calibrated.
557        """
558
559        if self.dl_path_loss and self.ul_path_loss:
560            self.log.info("Measurements are already calibrated.")
561
562        # Attach the phone to the base station
563        if not self.attach():
564            self.log.info(
565                "Skipping calibration because the phone failed to attach.")
566            return
567
568        # If downlink or uplink were not yet calibrated, do it now
569        if not self.dl_path_loss:
570            self.dl_path_loss = self.downlink_calibration()
571        if not self.ul_path_loss:
572            self.ul_path_loss = self.uplink_calibration()
573
574        # Detach after calibrating
575        self.detach()
576        time.sleep(2)
577
578    def start_traffic_for_calibration(self):
579        """
580            Starts UDP IP traffic before running calibration. Uses APN_1
581            configured in the phone.
582        """
583        self.simulator.start_data_traffic()
584
585    def stop_traffic_for_calibration(self):
586        """
587            Stops IP traffic after calibration.
588        """
589        self.simulator.stop_data_traffic()
590
591    def downlink_calibration(self, rat=None, power_units_conversion_func=None):
592        """ Computes downlink path loss and returns the calibration value
593
594        The DUT needs to be attached to the base station before calling this
595        method.
596
597        Args:
598            rat: desired RAT to calibrate (matching the label reported by
599                the phone)
600            power_units_conversion_func: a function to convert the units
601                reported by the phone to dBm. needs to take two arguments: the
602                reported signal level and bts. use None if no conversion is
603                needed.
604        Returns:
605            Dowlink calibration value and measured DL power.
606        """
607
608        # Check if this parameter was set. Child classes may need to override
609        # this class passing the necessary parameters.
610        if not rat:
611            raise ValueError(
612                "The parameter 'rat' has to indicate the RAT being used as "
613                "reported by the phone.")
614
615        # Save initial output level to restore it after calibration
616        restoration_config = self.BtsConfig()
617        restoration_config.output_power = self.primary_config.output_power
618
619        # Set BTS to a good output level to minimize measurement error
620        initial_screen_timeout = self.dut.droid.getScreenTimeout()
621        new_config = self.BtsConfig()
622        new_config.output_power = self.simulator.MAX_DL_POWER - 5
623        self.simulator.configure_bts(new_config)
624
625        # Set phone sleep time out
626        self.dut.droid.setScreenTimeout(1800)
627        self.dut.droid.goToSleepNow()
628        time.sleep(2)
629
630        # Starting IP traffic
631        self.start_traffic_for_calibration()
632
633        down_power_measured = []
634        for i in range(0, self.NUM_DL_CAL_READS):
635            # For some reason, the RSRP gets updated on Screen ON event
636            self.dut.droid.wakeUpNow()
637            time.sleep(4)
638            signal_strength = get_telephony_signal_strength(self.dut)
639            down_power_measured.append(signal_strength[rat])
640            self.dut.droid.goToSleepNow()
641            time.sleep(5)
642
643        # Stop IP traffic
644        self.stop_traffic_for_calibration()
645
646        # Reset phone and bts to original settings
647        self.dut.droid.goToSleepNow()
648        self.dut.droid.setScreenTimeout(initial_screen_timeout)
649        self.simulator.configure_bts(restoration_config)
650        time.sleep(2)
651
652        # Calculate the mean of the measurements
653        reported_asu_power = np.nanmean(down_power_measured)
654
655        # Convert from RSRP to signal power
656        if power_units_conversion_func:
657            avg_down_power = power_units_conversion_func(
658                reported_asu_power, self.primary_config)
659        else:
660            avg_down_power = reported_asu_power
661
662        # Calculate Path Loss
663        dl_target_power = self.simulator.MAX_DL_POWER - 5
664        down_call_path_loss = dl_target_power - avg_down_power
665
666        # Validate the result
667        if not 0 < down_call_path_loss < 100:
668            raise RuntimeError(
669                "Downlink calibration failed. The calculated path loss value "
670                "was {} dBm.".format(down_call_path_loss))
671
672        self.log.info(
673            "Measured downlink path loss: {} dB".format(down_call_path_loss))
674
675        return down_call_path_loss
676
677    def uplink_calibration(self):
678        """ Computes uplink path loss and returns the calibration value
679
680        The DUT needs to be attached to the base station before calling this
681        method.
682
683        Returns:
684            Uplink calibration value and measured UL power
685        """
686
687        # Save initial input level to restore it after calibration
688        restoration_config = self.BtsConfig()
689        restoration_config.input_power = self.primary_config.input_power
690
691        # Set BTS1 to maximum input allowed in order to perform
692        # uplink calibration
693        target_power = self.MAX_PHONE_OUTPUT_POWER
694        initial_screen_timeout = self.dut.droid.getScreenTimeout()
695        new_config = self.BtsConfig()
696        new_config.input_power = self.MAX_BTS_INPUT_POWER
697        self.simulator.configure_bts(new_config)
698
699        # Set phone sleep time out
700        self.dut.droid.setScreenTimeout(1800)
701        self.dut.droid.wakeUpNow()
702        time.sleep(2)
703
704        # Start IP traffic
705        self.start_traffic_for_calibration()
706
707        up_power_per_chain = []
708        # Get the number of chains
709        cmd = 'MONITOR? UL_PUSCH'
710        uplink_meas_power = self.anritsu.send_query(cmd)
711        str_power_chain = uplink_meas_power.split(',')
712        num_chains = len(str_power_chain)
713        for ichain in range(0, num_chains):
714            up_power_per_chain.append([])
715
716        for i in range(0, self.NUM_UL_CAL_READS):
717            uplink_meas_power = self.anritsu.send_query(cmd)
718            str_power_chain = uplink_meas_power.split(',')
719
720            for ichain in range(0, num_chains):
721                if (str_power_chain[ichain] == 'DEACTIVE'):
722                    up_power_per_chain[ichain].append(float('nan'))
723                else:
724                    up_power_per_chain[ichain].append(
725                        float(str_power_chain[ichain]))
726
727            time.sleep(3)
728
729        # Stop IP traffic
730        self.stop_traffic_for_calibration()
731
732        # Reset phone and bts to original settings
733        self.dut.droid.goToSleepNow()
734        self.dut.droid.setScreenTimeout(initial_screen_timeout)
735        self.simulator.configure_bts(restoration_config)
736        time.sleep(2)
737
738        # Phone only supports 1x1 Uplink so always chain 0
739        avg_up_power = np.nanmean(up_power_per_chain[0])
740        if np.isnan(avg_up_power):
741            raise RuntimeError(
742                "Calibration failed because the callbox reported the chain to "
743                "be deactive.")
744
745        up_call_path_loss = target_power - avg_up_power
746
747        # Validate the result
748        if not 0 < up_call_path_loss < 100:
749            raise RuntimeError(
750                "Uplink calibration failed. The calculated path loss value "
751                "was {} dBm.".format(up_call_path_loss))
752
753        self.log.info(
754            "Measured uplink path loss: {} dB".format(up_call_path_loss))
755
756        return up_call_path_loss
757
758    def load_pathloss_if_required(self):
759        """ If calibration is required, try to obtain the pathloss values from
760        the calibration table and measure them if they are not available. """
761        # Invalidate the previous values
762        self.dl_path_loss = None
763        self.ul_path_loss = None
764
765        # Load the new ones
766        if self.calibration_required:
767
768            band = self.primary_config.band
769
770            # Try loading the path loss values from the calibration table. If
771            # they are not available, use the automated calibration procedure.
772            try:
773                self.dl_path_loss = self.calibration_table[band]["dl"]
774                self.ul_path_loss = self.calibration_table[band]["ul"]
775            except KeyError:
776                self.calibrate(band)
777
778            # Complete the calibration table with the new values to be used in
779            # the next tests.
780            if band not in self.calibration_table:
781                self.calibration_table[band] = {}
782
783            if "dl" not in self.calibration_table[band] and self.dl_path_loss:
784                self.calibration_table[band]["dl"] = self.dl_path_loss
785
786            if "ul" not in self.calibration_table[band] and self.ul_path_loss:
787                self.calibration_table[band]["ul"] = self.ul_path_loss
788
789    def maximum_downlink_throughput(self):
790        """ Calculates maximum achievable downlink throughput in the current
791        simulation state.
792
793        Because thoughput is dependent on the RAT, this method needs to be
794        implemented by children classes.
795
796        Returns:
797            Maximum throughput in mbps
798        """
799        raise NotImplementedError()
800
801    def maximum_uplink_throughput(self):
802        """ Calculates maximum achievable downlink throughput in the current
803        simulation state.
804
805        Because thoughput is dependent on the RAT, this method needs to be
806        implemented by children classes.
807
808        Returns:
809            Maximum throughput in mbps
810        """
811        raise NotImplementedError()
812