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