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