• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3.4
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
21
22from acts.controllers.anritsu_lib._anritsu_utils import AnritsuError
23from acts.controllers.anritsu_lib.md8475a import BtsNumber
24from acts.test_utils.tel.tel_test_utils import get_telephony_signal_strength
25from acts.test_utils.tel.tel_test_utils import toggle_airplane_mode
26from acts.test_utils.tel.tel_test_utils import toggle_cell_data_roaming
27
28
29class BaseSimulation():
30    """ Base class for an Anritsu Simulation abstraction.
31
32    Classes that inherit from this base class implement different simulation
33    setups. The base class contains methods that are common to all simulation
34    configurations.
35
36    """
37
38    NUM_UL_CAL_READS = 3
39    NUM_DL_CAL_READS = 5
40    DL_CAL_TARGET_POWER = {'A': -15.0, 'B': -35.0}
41    MAX_BTS_INPUT_POWER = 30
42    MAX_PHONE_OUTPUT_POWER = 23
43    DL_MAX_POWER = {'A': -10.0, 'B': -30.0}
44    UL_MIN_POWER = -60.0
45
46    # Key to read the calibration setting from the test_config dictionary.
47    KEY_CALIBRATION = "calibration"
48
49    # Time in seconds to wait for the phone to settle
50    # after attaching to the base station.
51    SETTLING_TIME = 10
52
53    # Time in seconds to wait for the phone to attach
54    # to the basestation after toggling airplane mode.
55    ATTACH_WAITING_TIME = 120
56
57    # Max retries before giving up attaching the phone
58    ATTACH_MAX_RETRIES = 3
59
60    def __init__(self, anritsu, log, dut, test_config, calibration_table):
61        """ Initializes the Simulation object.
62
63        Keeps a reference to the callbox, log and dut handlers and
64        initializes the class attributes.
65
66        Args:
67            anritsu: the Anritsu callbox controller
68            log: a logger handle
69            dut: the android device handler
70            test_config: test configuration obtained from the config file
71            calibration_table: a dictionary containing path losses for
72                different bands.
73        """
74
75        self.anritsu = anritsu
76        self.log = log
77        self.dut = dut
78        self.calibration_table = calibration_table
79
80        # Turn calibration on or off depending on the test config value. If the
81        # key is not present, set to False by default
82        if self.KEY_CALIBRATION not in test_config:
83            self.log.warning("The '{} 'key is not set in the testbed "
84                             "parameters. Setting to off by default. To "
85                             "turn calibration on, include the key with "
86                             "a true/false value.".format(
87                                 self.KEY_CALIBRATION))
88
89        self.calibration_required = test_config.get(self.KEY_CALIBRATION,
90                                                    False)
91
92        # Gets BTS1 since this sim only has 1 BTS
93        self.bts1 = self.anritsu.get_BTS(BtsNumber.BTS1)
94
95        # Store the current calibrated band
96        self.current_calibrated_band = None
97
98        # Path loss measured during calibration
99        self.dl_path_loss = None
100        self.ul_path_loss = None
101
102        # Target signal levels obtained during configuration
103        self.sim_dl_power = None
104        self.sim_ul_power = None
105
106        # Set to default APN
107        log.info("Setting preferred APN to anritsu1.com.")
108        dut.droid.telephonySetAPN("anritsu1.com", "anritsu1.com")
109
110        # Enable roaming on the phone
111        toggle_cell_data_roaming(self.dut, True)
112
113    def start(self):
114        """ Start simulation.
115
116        Starts the simulation in the Anritsu Callbox.
117
118        """
119
120        # Make sure airplane mode is on so the phone won't attach right away
121        toggle_airplane_mode(self.log, self.dut, True)
122
123        # Wait for airplane mode setting to propagate
124        time.sleep(2)
125
126        # Start simulation if it wasn't started
127        self.anritsu.start_simulation()
128
129    def attach(self):
130        """ Attach the phone to the basestation.
131
132        Sets a good signal level, toggles airplane mode
133        and waits for the phone to attach.
134
135        Returns:
136            True if the phone was able to attach, False if not.
137        """
138
139        # Turn on airplane mode
140        toggle_airplane_mode(self.log, self.dut, True)
141
142        # Wait for airplane mode setting to propagate
143        time.sleep(2)
144
145        # Provide a good signal power for the phone to attach easily
146        self.bts1.input_level = -10
147        time.sleep(2)
148        self.bts1.output_level = -30
149
150        # Try to attach the phone.
151        for i in range(self.ATTACH_MAX_RETRIES):
152
153            try:
154
155                # Turn off airplane mode
156                toggle_airplane_mode(self.log, self.dut, False)
157
158                # Wait for the phone to attach.
159                self.anritsu.wait_for_registration_state(
160                    time_to_wait=self.ATTACH_WAITING_TIME)
161
162            except AnritsuError as e:
163
164                # The phone failed to attach
165                self.log.info(
166                    "UE failed to attach on attempt number {}.".format(i + 1))
167                self.log.info("Error message: {}".format(str(e)))
168
169                # Turn airplane mode on to prepare the phone for a retry.
170                toggle_airplane_mode(self.log, self.dut, True)
171
172                # Wait for APM to propagate
173                time.sleep(3)
174
175                # Retry
176                if i < self.ATTACH_MAX_RETRIES - 1:
177                    # Retry
178                    continue
179                else:
180                    # No more retries left. Return False.
181                    return False
182
183            else:
184                # The phone attached successfully.
185                time.sleep(self.SETTLING_TIME)
186                self.log.info("UE attached to the callbox.")
187                break
188
189        # Set signal levels obtained from the test parameters
190        if self.sim_dl_power:
191            self.set_downlink_rx_power(self.bts1, self.sim_dl_power)
192            time.sleep(2)
193        if self.sim_ul_power:
194            self.set_uplink_tx_power(self.bts1, self.sim_ul_power)
195            time.sleep(2)
196
197        return True
198
199    def detach(self):
200        """ Detach the phone from the basestation.
201
202        Turns airplane mode and resets basestation.
203        """
204
205        # Set the DUT to airplane mode so it doesn't see the
206        # cellular network going off
207        toggle_airplane_mode(self.log, self.dut, True)
208
209        # Wait for APM to propagate
210        time.sleep(2)
211
212        # Power off basestation
213        self.anritsu.set_simulation_state_to_poweroff()
214
215    def stop(self):
216        """  Detach phone from the basestation by stopping the simulation.
217
218        Send stop command to anritsu and turn on airplane mode.
219
220        """
221
222        # Set the DUT to airplane mode so it doesn't see the
223        # cellular network going off
224        toggle_airplane_mode(self.log, self.dut, True)
225
226        # Wait for APM to propagate
227        time.sleep(2)
228
229        # Stop the simulation
230        self.anritsu.stop_simulation()
231
232    def parse_parameters(self, parameters):
233        """ Configures simulation using a list of parameters.
234
235        Consumes parameters from a list.
236        Children classes need to call this method first.
237
238        Args:
239            parameters: list of parameters
240        """
241
242        pass
243
244    def consume_parameter(self, parameters, parameter_name, num_values=0):
245        """ Parses a parameter from a list.
246
247        Allows to parse the parameter list. Will delete parameters from the
248        list after consuming them to ensure that they are not used twice.
249
250        Args:
251            parameters: list of parameters
252            parameter_name: keyword to look up in the list
253            num_values: number of arguments following the
254                parameter name in the list
255        Returns:
256            A list containing the parameter name and the following num_values
257            arguments
258        """
259
260        try:
261            i = parameters.index(parameter_name)
262        except ValueError:
263            # parameter_name is not set
264            return []
265
266        return_list = []
267
268        try:
269            for j in range(num_values + 1):
270                return_list.append(parameters.pop(i))
271        except IndexError:
272            raise ValueError(
273                "Parameter {} has to be followed by {} values.".format(
274                    parameter_name, num_values))
275
276        return return_list
277
278    def set_downlink_rx_power(self, bts, signal_level):
279        """ Sets downlink rx power using calibration if available
280
281        Args:
282            bts: the base station in which to change the signal level
283            signal_level: desired downlink received power, can be either a
284            key value pair, an int or a float
285        """
286
287        # Obtain power value if the provided signal_level is a key value pair
288        if isinstance(signal_level, Enum):
289            power = signal_level.value
290        else:
291            power = signal_level
292
293        # Try to use measured path loss value. If this was not set, it will
294        # throw an TypeError exception
295        try:
296            calibrated_power = round(power + self.dl_path_loss)
297            if (calibrated_power >
298                    self.DL_MAX_POWER[self.anritsu._md8475_version]):
299                self.log.warning(
300                    "Cannot achieve phone DL Rx power of {} dBm. Requested TX "
301                    "power of {} dBm exceeds callbox limit!".format(
302                        power, calibrated_power))
303                calibrated_power = self.DL_MAX_POWER[
304                    self.anritsu._md8475_version]
305                self.log.warning(
306                    "Setting callbox Tx power to max possible ({} dBm)".format(
307                        calibrated_power))
308
309            self.log.info(
310                "Requested phone DL Rx power of {} dBm, setting callbox Tx "
311                "power at {} dBm".format(power, calibrated_power))
312            bts.output_level = calibrated_power
313            time.sleep(2)
314            # Power has to be a natural number so calibration wont be exact.
315            # Inform the actual received power after rounding.
316            self.log.info(
317                "Phone downlink received power is {0:.2f} dBm".format(
318                    calibrated_power - self.dl_path_loss))
319        except TypeError:
320            bts.output_level = round(power)
321            self.log.info("Phone downlink received power set to {} (link is "
322                          "uncalibrated).".format(round(power)))
323
324    def set_uplink_tx_power(self, bts, signal_level):
325        """ Sets uplink tx power using calibration if available
326
327        Args:
328            bts: the base station in which to change the signal level
329            signal_level: desired uplink transmitted power, can be either a
330            key value pair, an int or a float
331        """
332
333        # Obtain power value if the provided signal_level is a key value pair
334        if isinstance(signal_level, Enum):
335            power = signal_level.value
336        else:
337            power = signal_level
338
339        # Try to use measured path loss value. If this was not set, it will
340        # throw an TypeError exception
341        try:
342            calibrated_power = round(power - self.ul_path_loss)
343            if calibrated_power < self.UL_MIN_POWER:
344                self.log.warning(
345                    "Cannot achieve phone UL Tx power of {} dBm. Requested UL "
346                    "power of {} dBm exceeds callbox limit!".format(
347                        power, calibrated_power))
348                calibrated_power = self.UL_MIN_POWER
349                self.log.warning(
350                    "Setting UL Tx power to min possible ({} dBm)".format(
351                        calibrated_power))
352
353            self.log.info(
354                "Requested phone UL Tx power of {} dBm, setting callbox Rx "
355                "power at {} dBm".format(power, calibrated_power))
356            bts.input_level = calibrated_power
357            time.sleep(2)
358            # Power has to be a natural number so calibration wont be exact.
359            # Inform the actual transmitted power after rounding.
360            self.log.info(
361                "Phone uplink transmitted power is {0:.2f} dBm".format(
362                    calibrated_power + self.ul_path_loss))
363        except TypeError:
364            bts.input_level = round(power)
365            self.log.info("Phone uplink transmitted power set to {} (link is "
366                          "uncalibrated).".format(round(power)))
367
368    def calibrate(self):
369        """ Calculates UL and DL path loss if it wasn't done before.
370
371        """
372        # SET TBS pattern for calibration
373        self.bts1.tbs_pattern = "FULLALLOCATION" if self.tbs_pattern_on else "OFF"
374
375        if self.dl_path_loss and self.ul_path_loss:
376            self.log.info("Measurements are already calibrated.")
377
378        # Attach the phone to the base station
379        if not self.attach():
380            self.log.info(
381                "Skipping calibration because the phone failed to attach.")
382            return
383
384        # If downlink or uplink were not yet calibrated, do it now
385        if not self.dl_path_loss:
386            self.dl_path_loss = self.downlink_calibration(self.bts1)
387        if not self.ul_path_loss:
388            self.ul_path_loss = self.uplink_calibration(self.bts1)
389
390        # Detach after calibrating
391        self.detach()
392        time.sleep(2)
393
394    def downlink_calibration(self,
395                             bts,
396                             rat=None,
397                             power_units_conversion_func=None):
398        """ Computes downlink path loss and returns the calibration value
399
400        The bts needs to be set at the desired config (bandwidth, mode, etc)
401        before running the calibration. The phone also needs to be attached
402        to the desired basesation for calibration
403
404        Args:
405            bts: basestation handle
406            rat: desired RAT to calibrate (matching the label reported by
407                the phone)
408            power_units_conversion_func: a function to convert the units
409                reported by the phone to dBm. needs to take two arguments: the
410                reported signal level and bts. use None if no conversion is
411                needed.
412        Returns:
413            Dowlink calibration value and measured DL power.
414        """
415
416        # Check if this parameter was set. Child classes may need to override
417        # this class passing the necessary parameters.
418        if not rat:
419            raise ValueError(
420                "The parameter 'rat' has to indicate the RAT being used as "
421                "reported by the phone.")
422
423        # Set BTS to a good output level to minimize measurement error
424        init_output_level = bts.output_level
425        initial_screen_timeout = self.dut.droid.getScreenTimeout()
426        bts.output_level = self.DL_CAL_TARGET_POWER[
427            self.anritsu._md8475_version]
428
429        # Set phone sleep time out
430        self.dut.droid.setScreenTimeout(1800)
431        self.dut.droid.goToSleepNow()
432        time.sleep(2)
433
434        # Starting first the IP traffic (UDP): Using always APN 1
435        if not self.tbs_pattern_on:
436            try:
437                cmd = 'OPERATEIPTRAFFIC START,1'
438                self.anritsu.send_command(cmd)
439            except AnritsuError as inst:
440                self.log.warning(
441                    "{}\n".format(inst))  # Typically RUNNING already
442            time.sleep(4)
443
444        down_power_measured = []
445        for i in range(0, self.NUM_DL_CAL_READS):
446            # For some reason, the RSRP gets updated on Screen ON event
447            self.dut.droid.wakeUpNow()
448            time.sleep(4)
449            signal_strength = get_telephony_signal_strength(self.dut)
450            down_power_measured.append(signal_strength[rat])
451            self.dut.droid.goToSleepNow()
452            time.sleep(5)
453
454        # Stop the IP traffic (UDP)
455        if not self.tbs_pattern_on:
456            try:
457                cmd = 'OPERATEIPTRAFFIC STOP,1'
458                self.anritsu.send_command(cmd)
459            except AnritsuError as inst:
460                self.log.warning(
461                    "{}\n".format(inst))  # Typically STOPPED already
462            time.sleep(2)
463
464        # Reset phone and bts to original settings
465        self.dut.droid.goToSleepNow()
466        self.dut.droid.setScreenTimeout(initial_screen_timeout)
467        bts.output_level = init_output_level
468        time.sleep(2)
469
470        # Calculate the mean of the measurements
471        reported_asu_power = np.nanmean(down_power_measured)
472
473        # Convert from RSRP to signal power
474        if power_units_conversion_func:
475            avg_down_power = power_units_conversion_func(
476                reported_asu_power, bts)
477        else:
478            avg_down_power = reported_asu_power
479
480        # Calculate Path Loss
481        dl_target_power = self.DL_CAL_TARGET_POWER[
482            self.anritsu._md8475_version]
483        down_call_path_loss = dl_target_power - avg_down_power
484
485        # Validate the result
486        if not 0 < down_call_path_loss < 100:
487            raise RuntimeError(
488                "Downlink calibration failed. The calculated path loss value "
489                "was {} dBm.".format(down_call_path_loss))
490
491        self.log.info(
492            "Measured downlink path loss: {} dB".format(down_call_path_loss))
493
494        return down_call_path_loss
495
496    def uplink_calibration(self, bts):
497        """ Computes uplink path loss and returns the calibration value
498
499        The bts needs to be set at the desired config (bandwidth, mode, etc)
500        before running the calibration. The phone also neeeds to be attached
501        to the desired basesation for calibration
502
503        Args:
504            bts: basestation handle
505
506        Returns:
507            Uplink calibration value and measured UL power
508        """
509
510        # Set BTS1 to maximum input allowed in order to perform
511        # uplink calibration
512        target_power = self.MAX_PHONE_OUTPUT_POWER
513        initial_input_level = bts.input_level
514        initial_screen_timeout = self.dut.droid.getScreenTimeout()
515        bts.input_level = self.MAX_BTS_INPUT_POWER
516
517        # Set phone sleep time out
518        self.dut.droid.setScreenTimeout(1800)
519        self.dut.droid.wakeUpNow()
520        time.sleep(2)
521
522        # Starting first the IP traffic (UDP): Using always APN 1
523        if not self.tbs_pattern_on:
524            try:
525                cmd = 'OPERATEIPTRAFFIC START,1'
526                self.anritsu.send_command(cmd)
527            except AnritsuError as inst:
528                self.log.warning(
529                    "{}\n".format(inst))  # Typically RUNNING already
530            time.sleep(4)
531
532        up_power_per_chain = []
533        # Get the number of chains
534        cmd = 'MONITOR? UL_PUSCH'
535        uplink_meas_power = self.anritsu.send_query(cmd)
536        str_power_chain = uplink_meas_power.split(',')
537        num_chains = len(str_power_chain)
538        for ichain in range(0, num_chains):
539            up_power_per_chain.append([])
540
541        for i in range(0, self.NUM_UL_CAL_READS):
542            uplink_meas_power = self.anritsu.send_query(cmd)
543            str_power_chain = uplink_meas_power.split(',')
544
545            for ichain in range(0, num_chains):
546                if (str_power_chain[ichain] == 'DEACTIVE'):
547                    up_power_per_chain[ichain].append(float('nan'))
548                else:
549                    up_power_per_chain[ichain].append(
550                        float(str_power_chain[ichain]))
551
552            time.sleep(3)
553
554        # Stop the IP traffic (UDP)
555        if not self.tbs_pattern_on:
556            try:
557                cmd = 'OPERATEIPTRAFFIC STOP,1'
558                self.anritsu.send_command(cmd)
559            except AnritsuError as inst:
560                self.log.warning(
561                    "{}\n".format(inst))  # Typically STOPPED already
562            time.sleep(2)
563
564        # Reset phone and bts to original settings
565        self.dut.droid.goToSleepNow()
566        self.dut.droid.setScreenTimeout(initial_screen_timeout)
567        bts.input_level = initial_input_level
568        time.sleep(2)
569
570        # Phone only supports 1x1 Uplink so always chain 0
571        avg_up_power = np.nanmean(up_power_per_chain[0])
572        if np.isnan(avg_up_power):
573            raise RuntimeError(
574                "Calibration failed because the callbox reported the chain to "
575                "be deactive.")
576
577        up_call_path_loss = target_power - avg_up_power
578
579        # Validate the result
580        if not 0 < up_call_path_loss < 100:
581            raise RuntimeError(
582                "Uplink calibration failed. The calculated path loss value "
583                "was {} dBm.".format(up_call_path_loss))
584
585        self.log.info(
586            "Measured uplink path loss: {} dB".format(up_call_path_loss))
587
588        return up_call_path_loss
589
590    def set_band(self, bts, band, calibrate_if_necessary=True):
591        """ Sets the band used for communication.
592
593        When moving to a new band, recalibrate the link.
594
595        Args:
596            bts: basestation handle
597            band: desired band
598            calibrate_if_necessary: if False calibration will be skipped
599        """
600
601        bts.band = band
602        time.sleep(5)  # It takes some time to propagate the new band
603
604        # Invalidate the calibration values
605        self.dl_path_loss = None
606        self.ul_path_loss = None
607
608        # Only calibrate when required.
609        if self.calibration_required and calibrate_if_necessary:
610            # Try loading the path loss values from the calibration table. If
611            # they are not available, use the automated calibration procedure.
612            try:
613                self.dl_path_loss = self.calibration_table[band]["dl"]
614                self.ul_path_loss = self.calibration_table[band]["ul"]
615            except KeyError:
616                self.calibrate()
617
618            # Complete the calibration table with the new values to be used in
619            # the next tests.
620            if band not in self.calibration_table:
621                self.calibration_table[band] = {}
622
623            if "dl" not in self.calibration_table[band] and self.dl_path_loss:
624                self.calibration_table[band]["dl"] = self.dl_path_loss
625
626            if "ul" not in self.calibration_table[band] and self.ul_path_loss:
627                self.calibration_table[band]["ul"] = self.ul_path_loss
628
629    def maximum_downlink_throughput(self):
630        """ Calculates maximum achievable downlink throughput in the current
631        simulation state.
632
633        Because thoughput is dependent on the RAT, this method needs to be
634        implemented by children classes.
635
636        Returns:
637            Maximum throughput in mbps
638        """
639        raise NotImplementedError()
640
641    def maximum_uplink_throughput(self):
642        """ Calculates maximum achievable downlink throughput in the current
643        simulation state.
644
645        Because thoughput is dependent on the RAT, this method needs to be
646        implemented by children classes.
647
648        Returns:
649            Maximum throughput in mbps
650        """
651        raise NotImplementedError()
652
653    def start_test_case(self):
654        """ Starts a test case in the current simulation.
655
656        Requires the phone to be attached.
657        """
658
659        pass
660