diff options
77 files changed, 4290 insertions, 2646 deletions
diff --git a/acts/framework/acts/base_test.py b/acts/framework/acts/base_test.py index c76d98a3ca..6fad51ea6c 100755 --- a/acts/framework/acts/base_test.py +++ b/acts/framework/acts/base_test.py @@ -277,14 +277,6 @@ class BaseTestClass(MoblyBaseTest): A list of json serializable objects, each represents the info of a controller object. The order of the info object should follow that of the input objects. - def get_post_job_info(controller_list): - [Optional] Returns information about the controller after the - test has run. This info is sent to test_run_summary.json's - "Extras" key. - Args: - The list of controller objects created by the module - Returns: - A (name, data) tuple. Registering a controller module declares a test class's dependency the controller. If the module config exists and the module matches the controller interface, controller objects will be instantiated with @@ -338,28 +330,6 @@ class BaseTestClass(MoblyBaseTest): setattr(self, module_ref_name, controllers) return controllers - def unregister_controllers(self): - """Destroy controller objects and clear internal registry. Invokes - Mobly's controller manager's unregister_controllers. - - This will be called upon test class teardown. - """ - controller_modules = self._controller_manager._controller_modules - controller_objects = self._controller_manager._controller_objects - # Record post job info for the controller - for name, controller_module in controller_modules.items(): - if hasattr(controller_module, 'get_post_job_info'): - self.log.debug('Getting post job info for %s', name) - try: - name, value = controller_module.get_post_job_info( - controller_objects[name]) - self.results.set_extra_data(name, value) - self.summary_writer.dump( - {name: value}, records.TestSummaryEntryType.USER_DATA) - except: - self.log.error("Fail to get post job info for %s", name) - self._controller_manager.unregister_controllers() - def _record_controller_info(self): """Collect controller information and write to summary file.""" try: @@ -387,8 +357,7 @@ class BaseTestClass(MoblyBaseTest): """Proxy function to guarantee the base implementation of teardown_class is called. """ - self.teardown_class() - self.unregister_controllers() + super()._teardown_class() event_bus.post(TestClassEndEvent(self, self.results)) def _setup_test(self, test_name): diff --git a/acts/framework/acts/bin/monsoon.py b/acts/framework/acts/bin/monsoon.py index c43eca505e..a76b425dd9 100755 --- a/acts/framework/acts/bin/monsoon.py +++ b/acts/framework/acts/bin/monsoon.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright 2016 - The Android Open Source Project +# Copyright 2019 - The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,75 +13,100 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Interface for a USB-connected Monsoon power meter (http://msoon.com/LabEquipment/PowerMonitor/). """ import argparse -import sys -import time -import collections -from acts.controllers.monsoon import Monsoon +import acts.controllers.monsoon as monsoon_controller + -def main(FLAGS): +def main(args): """Simple command-line interface for Monsoon.""" - if FLAGS.avg and FLAGS.avg < 0: - print("--avg must be greater than 0") + if args.avg and args.avg < 0: + print('--avg must be greater than 0') return - mon = Monsoon(serial=int(FLAGS.serialno[0])) + mon = monsoon_controller.create([int(args.serialno[0])])[0] - if FLAGS.voltage is not None: - mon.set_voltage(FLAGS.voltage) + if args.voltage is not None: + mon.set_voltage(args.voltage) - if FLAGS.current is not None: - mon.set_max_current(FLAGS.current) + if args.current is not None: + mon.set_max_current(args.current) - if FLAGS.status: + if args.status: items = sorted(mon.status.items()) - print("\n".join(["%s: %s" % item for item in items])) + print('\n'.join(['%s: %s' % item for item in items])) - if FLAGS.usbpassthrough: - mon.usb(FLAGS.usbpassthrough) + if args.usbpassthrough: + mon.usb(args.usbpassthrough) - if FLAGS.startcurrent is not None: - mon.set_max_init_current(FLAGS.startcurrent) + if args.startcurrent is not None: + mon.set_max_initial_current(args.startcurrent) - if FLAGS.samples: - # Have to sleep a bit here for monsoon to be ready to lower the rate of - # socket read timeout. - time.sleep(1) - result = mon.take_samples(FLAGS.hz, FLAGS.samples, - sample_offset=FLAGS.offset, live=True) + if args.samples: + result = mon.measure_power( + args.samples / args.hz, + measure_after_seconds=args.offset, + hz=args.hz, + output_path='monsoon_output.txt') print(repr(result)) + if __name__ == '__main__': - parser = argparse.ArgumentParser(description=("This is a python utility " - "tool to control monsoon power measurement boxes.")) - parser.add_argument("--status", action="store_true", - help="Print power meter status.") - parser.add_argument("-avg", "--avg", type=int, default=0, - help="Also report average over last n data points.") - parser.add_argument("-v", "--voltage", type=float, - help="Set output voltage (0 for off)") - parser.add_argument("-c", "--current", type=float, - help="Set max output current.") - parser.add_argument("-sc", "--startcurrent", type=float, - help="Set max power-up/inital current.") - parser.add_argument("-usb", "--usbpassthrough", choices=("on", "off", - "auto"), help="USB control (on, off, auto).") - parser.add_argument("-sp", "--samples", type=int, - help="Collect and print this many samples") - parser.add_argument("-hz", "--hz", type=int, - help="Sample this many times per second.") - parser.add_argument("-d", "--device", help="Use this /dev/ttyACM... file.") - parser.add_argument("-sn", "--serialno", type=int, nargs=1, required=True, - help="The serial number of the Monsoon to use.") - parser.add_argument("--offset", type=int, nargs='?', default=0, - help="The number of samples to discard when calculating average.") - parser.add_argument("-r", "--ramp", action="store_true", help=("Gradually " - "increase voltage to prevent tripping Monsoon overvoltage")) - args = parser.parse_args() - main(args) + parser = argparse.ArgumentParser( + description='This is a python utility tool to control monsoon power ' + 'measurement boxes.') + parser.add_argument( + '--status', action='store_true', help='Print power meter status.') + parser.add_argument( + '-avg', + '--avg', + type=int, + default=0, + help='Also report average over last n data points.') + parser.add_argument( + '-v', '--voltage', type=float, help='Set output voltage (0 for off)') + parser.add_argument( + '-c', '--current', type=float, help='Set max output current.') + parser.add_argument( + '-sc', + '--startcurrent', + type=float, + help='Set max power-up/initial current.') + parser.add_argument( + '-usb', + '--usbpassthrough', + choices=('on', 'off', 'auto'), + help='USB control (on, off, auto).') + parser.add_argument( + '-sp', + '--samples', + type=int, + help='Collect and print this many samples') + parser.add_argument( + '-hz', '--hz', type=int, help='Sample this many times per second.') + parser.add_argument('-d', '--device', help='Use this /dev/ttyACM... file.') + parser.add_argument( + '-sn', + '--serialno', + type=int, + nargs=1, + required=True, + help='The serial number of the Monsoon to use.') + parser.add_argument( + '--offset', + type=int, + nargs='?', + default=0, + help='The number of samples to discard when calculating average.') + parser.add_argument( + '-r', + '--ramp', + action='store_true', + help='Gradually increase voltage to prevent tripping Monsoon ' + 'overvoltage.') + arguments = parser.parse_args() + main(arguments) diff --git a/acts/framework/acts/controllers/android_device.py b/acts/framework/acts/controllers/android_device.py index 9f03c93e50..16671fa621 100755 --- a/acts/framework/acts/controllers/android_device.py +++ b/acts/framework/acts/controllers/android_device.py @@ -137,18 +137,6 @@ def get_info(ads): return device_info -def get_post_job_info(ads): - """Returns the tracked build id to test_run_summary.json - - Args: - ads: A list of AndroidDevice objects. - - Returns: - A dict consisting of {'build_id': ads[0].build_info} - """ - return 'Build Info', ads[0].build_info - - def _start_services_on_ads(ads): """Starts long running services on multiple AndroidDevice objects. diff --git a/acts/framework/acts/controllers/anritsu_lib/md8475_cellular_simulator.py b/acts/framework/acts/controllers/anritsu_lib/md8475_cellular_simulator.py index 23fee412cb..08db20b52f 100644 --- a/acts/framework/acts/controllers/anritsu_lib/md8475_cellular_simulator.py +++ b/acts/framework/acts/controllers/anritsu_lib/md8475_cellular_simulator.py @@ -177,6 +177,17 @@ class MD8475CellularSimulator(cc.AbstractCellularSimulator): else: self.bts[bts_index].tbs_pattern = 'OFF' + def set_lte_rrc_state_change_timer(self, enabled, time=10): + """ Configures the LTE RRC state change timer. + + Args: + enabled: a boolean indicating if the timer should be on or off. + time: time in seconds for the timer to expire + """ + self.anritsu.set_lte_rrc_status_change(enabled) + if enabled: + self.anritsu.set_lte_rrc_status_change_timer(time) + def set_band(self, bts_index, band): """ Sets the right duplex mode before switching to a new band. diff --git a/acts/framework/acts/controllers/ap_lib/hostapd_ap_preset.py b/acts/framework/acts/controllers/ap_lib/hostapd_ap_preset.py index 930d525574..a7d89a7e5b 100644 --- a/acts/framework/acts/controllers/ap_lib/hostapd_ap_preset.py +++ b/acts/framework/acts/controllers/ap_lib/hostapd_ap_preset.py @@ -13,6 +13,8 @@ # limitations under the License. import acts.controllers.ap_lib.third_party_ap_profiles.actiontec as actiontec +import acts.controllers.ap_lib.third_party_ap_profiles.asus as asus +import acts.controllers.ap_lib.third_party_ap_profiles.belkin as belkin from acts.controllers.ap_lib import hostapd_config from acts.controllers.ap_lib import hostapd_constants @@ -243,10 +245,32 @@ def create_ap_preset(profile_name='whirlwind', security=security) elif profile_name == 'actiontec_mi424wr': config = actiontec.actiontec_mi424wr(iface_wlan_2g=iface_wlan_2g, - iface_wlan_5g=iface_wlan_5g, channel=channel, ssid=ssid, security=security) + elif profile_name == 'asus_rtac66u': + config = asus.asus_rtac66u(iface_wlan_2g=iface_wlan_2g, + iface_wlan_5g=iface_wlan_5g, + channel=channel, + ssid=ssid, + security=security) + elif profile_name == 'asus_rtac86u': + config = asus.asus_rtac86u(iface_wlan_2g=iface_wlan_2g, + iface_wlan_5g=iface_wlan_5g, + channel=channel, + ssid=ssid, + security=security) + elif profile_name == 'asus_rtac5300': + config = asus.asus_rtac5300(iface_wlan_2g=iface_wlan_2g, + iface_wlan_5g=iface_wlan_5g, + channel=channel, + ssid=ssid, + security=security) + elif profile_name == 'belkin_f9k1001v5': + config = belkin.belkin_f9k1001v5(iface_wlan_2g=iface_wlan_2g, + channel=channel, + ssid=ssid, + security=security) else: raise ValueError('Invalid ap model specified (%s)' % profile_name) diff --git a/acts/framework/acts/controllers/ap_lib/hostapd_config.py b/acts/framework/acts/controllers/ap_lib/hostapd_config.py index 2ee6f0dadf..64ea022641 100644 --- a/acts/framework/acts/controllers/ap_lib/hostapd_config.py +++ b/acts/framework/acts/controllers/ap_lib/hostapd_config.py @@ -71,7 +71,6 @@ class HostapdConfig(object): All the settings for a router that are not part of an ssid. """ - def _get_11ac_center_channel_from_channel(self, channel): """Returns the center channel of the selected channel band based on the channel and channel bandwidth provided. @@ -452,10 +451,9 @@ class HostapdConfig(object): logging.warning( 'No channel bandwidth specified. Using 80MHz for 11ac.') self._vht_oper_chwidth = 1 - if not vht_channel_width == 20: - if not vht_center_channel: - self._vht_oper_centr_freq_seg0_idx = self._get_11ac_center_channel_from_channel( - self.channel) + if not vht_channel_width == 20 and not vht_center_channel: + self._vht_oper_centr_freq_seg0_idx = self._get_11ac_center_channel_from_channel( + self.channel) else: self._vht_oper_centr_freq_seg0_idx = vht_center_channel self._ac_capabilities = set(ac_capabilities) @@ -473,16 +471,16 @@ class HostapdConfig(object): self._bss_lookup[bss.name] = bss def __repr__(self): - return ('%s(mode=%r, channel=%r, frequency=%r, ' - 'n_capabilities=%r, beacon_interval=%r, ' - 'dtim_period=%r, frag_threshold=%r, ssid=%r, bssid=%r, ' - 'wmm_enabled=%r, security_config=%r, ' - 'spectrum_mgmt_required=%r)' % - (self.__class__.__name__, self._mode, self.channel, - self.frequency, self._n_capabilities, self._beacon_interval, - self._dtim_period, self._frag_threshold, self._ssid, - self._bssid, self._wmm_enabled, self._security, - self._spectrum_mgmt_required)) + return ( + '%s(mode=%r, channel=%r, frequency=%r, ' + 'n_capabilities=%r, beacon_interval=%r, ' + 'dtim_period=%r, frag_threshold=%r, ssid=%r, bssid=%r, ' + 'wmm_enabled=%r, security_config=%r, ' + 'spectrum_mgmt_required=%r)' % + (self.__class__.__name__, self._mode, self.channel, self.frequency, + self._n_capabilities, self._beacon_interval, self._dtim_period, + self._frag_threshold, self._ssid, self._bssid, self._wmm_enabled, + self._security, self._spectrum_mgmt_required)) def supports_channel(self, value): """Check whether channel is supported by the current hardware mode. diff --git a/acts/framework/acts/controllers/ap_lib/hostapd_constants.py b/acts/framework/acts/controllers/ap_lib/hostapd_constants.py index 6fe3530d4b..4139ed0595 100755 --- a/acts/framework/acts/controllers/ap_lib/hostapd_constants.py +++ b/acts/framework/acts/controllers/ap_lib/hostapd_constants.py @@ -153,7 +153,7 @@ N_CAPABILITY_LSIG_TXOP_PROT = object() N_CAPABILITY_40_INTOLERANT = object() N_CAPABILITY_MAX_AMSDU_7935 = object() N_CAPABILITY_DELAY_BLOCK_ACK = object() -N_CAPABILITY_SMPS_STATIC=object() +N_CAPABILITY_SMPS_STATIC = object() N_CAPABILITY_SMPS_DYNAMIC = object() N_CAPABILITIES_MAPPING = { N_CAPABILITY_LDPC: '[LDPC]', @@ -262,12 +262,12 @@ VHT_CHANNEL = { HT40_ALLOW_MAP = { N_CAPABILITY_HT40_MINUS_CHANNELS: tuple( - itertools.chain( - range(6, 14), range(40, 65, 8), range(104, 137, 8), [153, 161])), + itertools.chain(range(6, 14), range(40, 65, 8), range(104, 137, 8), + [153, 161])), N_CAPABILITY_HT40_PLUS_CHANNELS: tuple( - itertools.chain( - range(1, 8), range(36, 61, 8), range(100, 133, 8), [149, 157])) + itertools.chain(range(1, 8), range(36, 61, 8), range(100, 133, 8), + [149, 157])) } PMF_SUPPORT_DISABLED = 0 @@ -280,15 +280,17 @@ DRIVER_NAME = 'nl80211' CENTER_CHANNEL_MAP = { VHT_CHANNEL_WIDTH_40: { - 'delta': 2, + 'delta': + 2, 'channels': ((36, 40), (44, 48), (52, 56), (60, 64), (100, 104), (108, 112), (116, 120), (124, 128), (132, 136), (140, 144), (149, 153), (147, 161)) }, VHT_CHANNEL_WIDTH_80: { - 'delta': 6, - 'channels': ((36, 48), (52, 64), (100, 112), (116, 128), (132, 144), - (149, 161)) + 'delta': + 6, + 'channels': + ((36, 48), (52, 64), (100, 112), (116, 128), (132, 144), (149, 161)) }, VHT_CHANNEL_WIDTH_160: { 'delta': 14, @@ -296,26 +298,24 @@ CENTER_CHANNEL_MAP = { } } -OFDM_DATA_RATES = { - 'supported_rates': '60 90 120 180 240 360 480 540' -} +OFDM_DATA_RATES = {'supported_rates': '60 90 120 180 240 360 480 540'} -CCK_DATA_RATES = { - 'supported_rates': '10 20 55 11' -} +CCK_DATA_RATES = {'supported_rates': '10 20 55 11'} -OFDM_ONLY_BASIC_RATES = { - 'basic_rates': '60 120 240' -} +OFDM_ONLY_BASIC_RATES = {'basic_rates': '60 120 240'} -CCK_AND_OFDM_BASIC_RATES = { - 'basic_rates': '10 20 55 11' -} +CCK_AND_OFDM_BASIC_RATES = {'basic_rates': '10 20 55 11'} WEP_AUTH = { - 'open': {'auth_algs': 1}, - 'shared': {'auth_algs': 2}, - 'open_and_shared': {'auth_algs': 3} + 'open': { + 'auth_algs': 1 + }, + 'shared': { + 'auth_algs': 2 + }, + 'open_and_shared': { + 'auth_algs': 3 + } } WMM_11B_DEFAULT_PARAMS = { @@ -326,7 +326,7 @@ WMM_11B_DEFAULT_PARAMS = { 'wmm_ac_be_aifs': 3, 'wmm_ac_be_cwmin': 5, 'wmm_ac_be_cwmax': 7, - 'wmm_ac_be_txop_limit':0, + 'wmm_ac_be_txop_limit': 0, 'wmm_ac_vi_aifs': 2, 'wmm_ac_vi_cwmin': 4, 'wmm_ac_vi_cwmax': 5, @@ -345,7 +345,7 @@ WMM_PHYS_11A_11G_11N_11AC_DEFAULT_PARAMS = { 'wmm_ac_be_aifs': 3, 'wmm_ac_be_cwmin': 4, 'wmm_ac_be_cwmax': 10, - 'wmm_ac_be_txop_limit':0, + 'wmm_ac_be_txop_limit': 0, 'wmm_ac_vi_aifs': 2, 'wmm_ac_vi_cwmin': 3, 'wmm_ac_vi_cwmax': 4, @@ -364,7 +364,7 @@ WMM_NON_DEFAULT_PARAMS = { 'wmm_ac_be_aifs': 2, 'wmm_ac_be_cwmin': 2, 'wmm_ac_be_cwmax': 8, - 'wmm_ac_be_txop_limit':0, + 'wmm_ac_be_txop_limit': 0, 'wmm_ac_vi_aifs': 1, 'wmm_ac_vi_cwmin': 7, 'wmm_ac_vi_cwmax': 10, @@ -380,295 +380,812 @@ WMM_ACM_BE = {'wmm_ac_be_acm': 1} WMM_ACM_VI = {'wmm_ac_vi_acm': 1} WMM_ACM_VO = {'wmm_ac_vo_acm': 1} -UTF_8_SSID = { - 'utf8_ssid': 1 -} +UAPSD_ENABLED = {'uapsd_advertisement_enabled': 1} + +UTF_8_SSID = {'utf8_ssid': 1} VENDOR_IE = { - 'correct_length_beacon': - {'vendor_elements': 'dd0411223301'} , - 'too_short_length_beacon': - {'vendor_elements': 'dd0311223301'}, - 'too_long_length_beacon': - {'vendor_elements': 'dd0511223301'}, - 'zero_length_beacon_with_data': - {'vendor_elements': 'dd0011223301'}, - 'zero_length_beacon_without_data': - {'vendor_elements': 'dd00'}, - 'simliar_to_wpa': - {'vendor_elements': 'dd040050f203'}, - 'correct_length_association_response': - {'assocresp_elements=': 'dd0411223301'}, - 'too_short_length_association_response': - {'assocresp_elements=': 'dd0311223301'}, - 'too_long_length_association_response': - {'assocresp_elements=': 'dd0511223301'}, - 'zero_length_association_response_with_data': - {'assocresp_elements': 'dd0011223301'}, - 'zero_length_association_response_without_data': - {'assocresp_elements': 'dd00'} + 'correct_length_beacon': { + 'vendor_elements': 'dd0411223301' + }, + 'too_short_length_beacon': { + 'vendor_elements': 'dd0311223301' + }, + 'too_long_length_beacon': { + 'vendor_elements': 'dd0511223301' + }, + 'zero_length_beacon_with_data': { + 'vendor_elements': 'dd0011223301' + }, + 'zero_length_beacon_without_data': { + 'vendor_elements': 'dd00' + }, + 'simliar_to_wpa': { + 'vendor_elements': 'dd040050f203' + }, + 'correct_length_association_response': { + 'assocresp_elements=': 'dd0411223301' + }, + 'too_short_length_association_response': { + 'assocresp_elements=': 'dd0311223301' + }, + 'too_long_length_association_response': { + 'assocresp_elements=': 'dd0511223301' + }, + 'zero_length_association_response_with_data': { + 'assocresp_elements': 'dd0011223301' + }, + 'zero_length_association_response_without_data': { + 'assocresp_elements': 'dd00' + } } -ENABLE_IEEE80211D = { - 'ieee80211d': 1 -} +ENABLE_IEEE80211D = {'ieee80211d': 1} COUNTRY_STRING = { - 'ALL': {'country3': '0x20'}, - 'OUTDOOR': {'country3': '0x4f'}, - 'INDOOR': {'country3': '0x49'}, - 'NONCOUNTRY': {'country3': '0x58'}, - 'GLOBAL': {'country3': '0x04'} + 'ALL': { + 'country3': '0x20' + }, + 'OUTDOOR': { + 'country3': '0x4f' + }, + 'INDOOR': { + 'country3': '0x49' + }, + 'NONCOUNTRY': { + 'country3': '0x58' + }, + 'GLOBAL': { + 'country3': '0x04' + } } COUNTRY_CODE = { - 'AFGHANISTAN': {'country_code': 'AF'}, - 'ALAND_ISLANDS': {'country_code': 'AX'}, - 'ALBANIA': {'country_code': 'AL'}, - 'ALGERIA': {'country_code': 'DZ'}, - 'AMERICAN_SAMOA': {'country_code': 'AS'}, - 'ANDORRA': {'country_code': 'AD'}, - 'ANGOLA': {'country_code': 'AO'}, - 'ANGUILLA': {'country_code': 'AI'}, - 'ANTARCTICA': {'country_code': 'AQ'}, - 'ANTIGUA_AND_BARBUDA': {'country_code': 'AG'}, - 'ARGENTINA': {'country_code': 'AR'}, - 'ARMENIA': {'country_code': 'AM'}, - 'ARUBA': {'country_code': 'AW'}, - 'AUSTRALIA': {'country_code': 'AU'}, - 'AUSTRIA': {'country_code': 'AT'}, - 'AZERBAIJAN': {'country_code': 'AZ'}, - 'BAHAMAS': {'country_code': 'BS'}, - 'BAHRAIN': {'country_code': 'BH'}, - 'BANGLADESH': {'country_code': 'BD'}, - 'BARBADOS': {'country_code': 'BB'}, - 'BELARUS': {'country_code': 'BY'}, - 'BELGIUM': {'country_code': 'BE'}, - 'BELIZE': {'country_code': 'BZ'}, - 'BENIN': {'country_code': 'BJ'}, - 'BERMUDA': {'country_code': 'BM'}, - 'BHUTAN': {'country_code': 'BT'}, - 'BOLIVIA': {'country_code': 'BO'}, - 'BONAIRE': {'country_code': 'BQ'}, - 'BOSNIA_AND_HERZEGOVINA': {'country_code': 'BA'}, - 'BOTSWANA': {'country_code': 'BW'}, - 'BOUVET_ISLAND': {'country_code': 'BV'}, - 'BRAZIL': {'country_code': 'BR'}, - 'BRITISH_INDIAN_OCEAN_TERRITORY': {'country_code': 'IO'}, - 'BRUNEI_DARUSSALAM': {'country_code': 'BN'}, - 'BULGARIA': {'country_code': 'BG'}, - 'BURKINA_FASO': {'country_code': 'BF'}, - 'BURUNDI': {'country_code': 'BI'}, - 'CAMBODIA': {'country_code': 'KH'}, - 'CAMEROON': {'country_code': 'CM'}, - 'CANADA': {'country_code': 'CA'}, - 'CAPE_VERDE': {'country_code': 'CV'}, - 'CAYMAN_ISLANDS': {'country_code': 'KY'}, - 'CENTRAL_AFRICAN_REPUBLIC': {'country_code': 'CF'}, - 'CHAD': {'country_code': 'TD'}, - 'CHILE': {'country_code': 'CL'}, - 'CHINA': {'country_code': 'CN'}, - 'CHRISTMAS_ISLAND': {'country_code': 'CX'}, - 'COCOS_ISLANDS': {'country_code': 'CC'}, - 'COLOMBIA': {'country_code': 'CO'}, - 'COMOROS': {'country_code': 'KM'}, - 'CONGO': {'country_code': 'CG'}, - 'DEMOCRATIC_REPUBLIC_CONGO': {'country_code': 'CD'}, - 'COOK_ISLANDS': {'country_code': 'CK'}, - 'COSTA_RICA': {'country_code': 'CR'}, - 'COTE_D_IVOIRE': {'country_code': 'CI'}, - 'CROATIA': {'country_code': 'HR'}, - 'CUBA': {'country_code': 'CU'}, - 'CURACAO': {'country_code': 'CW'}, - 'CYPRUS': {'country_code': 'CY'}, - 'CZECH_REPUBLIC': {'country_code': 'CZ'}, - 'DENMARK': {'country_code': 'DK'}, - 'DJIBOUTI': {'country_code': 'DJ'}, - 'DOMINICA': {'country_code': 'DM'}, - 'DOMINICAN_REPUBLIC': {'country_code': 'DO'}, - 'ECUADOR': {'country_code': 'EC'}, - 'EGYPT': {'country_code': 'EG'}, - 'EL_SALVADOR': {'country_code': 'SV'}, - 'EQUATORIAL_GUINEA': {'country_code': 'GQ'}, - 'ERITREA': {'country_code': 'ER'}, - 'ESTONIA': {'country_code': 'EE'}, - 'ETHIOPIA': {'country_code': 'ET'}, - 'FALKLAND_ISLANDS_(MALVINAS)': {'country_code': 'FK'}, - 'FAROE_ISLANDS': {'country_code': 'FO'}, - 'FIJI': {'country_code': 'FJ'}, - 'FINLAND': {'country_code': 'FI'}, - 'FRANCE': {'country_code': 'FR'}, - 'FRENCH_GUIANA': {'country_code': 'GF'}, - 'FRENCH_POLYNESIA': {'country_code': 'PF'}, - 'FRENCH_SOUTHERN_TERRITORIES': {'country_code': 'TF'}, - 'GABON': {'country_code': 'GA'}, - 'GAMBIA': {'country_code': 'GM'}, - 'GEORGIA': {'country_code': 'GE'}, - 'GERMANY': {'country_code': 'DE'}, - 'GHANA': {'country_code': 'GH'}, - 'GIBRALTAR': {'country_code': 'GI'}, - 'GREECE': {'country_code': 'GR'}, - 'GREENLAND': {'country_code': 'GL'}, - 'GRENADA': {'country_code': 'GD'}, - 'GUADELOUPE': {'country_code': 'GP'}, - 'GUAM': {'country_code': 'GU'}, - 'GUATEMALA': {'country_code': 'GT'}, - 'GUERNSEY': {'country_code': 'GG'}, - 'GUINEA': {'country_code': 'GN'}, - 'GUINEA-BISSAU': {'country_code': 'GW'}, - 'GUYANA': {'country_code': 'GY'}, - 'HAITI': {'country_code': 'HT'}, - 'HEARD_ISLAND_AND_MCDONALD_ISLANDS': {'country_code': 'HM'}, - 'VATICAN_CITY_STATE': {'country_code': 'VA'}, - 'HONDURAS': {'country_code': 'HN'}, - 'HONG_KONG': {'country_code': 'HK'}, - 'HUNGARY': {'country_code': 'HU'}, - 'ICELAND': {'country_code': 'IS'}, - 'INDIA': {'country_code': 'IN'}, - 'INDONESIA': {'country_code': 'ID'}, - 'IRAN': {'country_code': 'IR'}, - 'IRAQ': {'country_code': 'IQ'}, - 'IRELAND': {'country_code': 'IE'}, - 'ISLE_OF_MAN': {'country_code': 'IM'}, - 'ISRAEL': {'country_code': 'IL'}, - 'ITALY': {'country_code': 'IT'}, - 'JAMAICA': {'country_code': 'JM'}, - 'JAPAN': {'country_code': 'JP'}, - 'JERSEY': {'country_code': 'JE'}, - 'JORDAN': {'country_code': 'JO'}, - 'KAZAKHSTAN': {'country_code': 'KZ'}, - 'KENYA': {'country_code': 'KE'}, - 'KIRIBATI': {'country_code': 'KI'}, - 'DEMOCRATIC_PEOPLE_S_REPUBLIC_OF_KOREA': {'country_code': 'KP'}, - 'REPUBLIC_OF_KOREA': {'country_code': 'KR'}, - 'KUWAIT': {'country_code': 'KW'}, - 'KYRGYZSTAN': {'country_code': 'KG'}, - 'LAO': {'country_code': 'LA'}, - 'LATVIA': {'country_code': 'LV'}, - 'LEBANON': {'country_code': 'LB'}, - 'LESOTHO': {'country_code': 'LS'}, - 'LIBERIA': {'country_code': 'LR'}, - 'LIBYA': {'country_code': 'LY'}, - 'LIECHTENSTEIN': {'country_code': 'LI'}, - 'LITHUANIA': {'country_code': 'LT'}, - 'LUXEMBOURG': {'country_code': 'LU'}, - 'MACAO': {'country_code': 'MO'}, - 'MACEDONIA': {'country_code': 'MK'}, - 'MADAGASCAR': {'country_code': 'MG'}, - 'MALAWI': {'country_code': 'MW'}, - 'MALAYSIA': {'country_code': 'MY'}, - 'MALDIVES': {'country_code': 'MV'}, - 'MALI': {'country_code': 'ML'}, - 'MALTA': {'country_code': 'MT'}, - 'MARSHALL_ISLANDS': {'country_code': 'MH'}, - 'MARTINIQUE': {'country_code': 'MQ'}, - 'MAURITANIA': {'country_code': 'MR'}, - 'MAURITIUS': {'country_code': 'MU'}, - 'MAYOTTE': {'country_code': 'YT'}, - 'MEXICO': {'country_code': 'MX'}, - 'MICRONESIA': {'country_code': 'FM'}, - 'MOLDOVA': {'country_code': 'MD'}, - 'MONACO': {'country_code': 'MC'}, - 'MONGOLIA': {'country_code': 'MN'}, - 'MONTENEGRO': {'country_code': 'ME'}, - 'MONTSERRAT': {'country_code': 'MS'}, - 'MOROCCO': {'country_code': 'MA'}, - 'MOZAMBIQUE': {'country_code': 'MZ'}, - 'MYANMAR': {'country_code': 'MM'}, - 'NAMIBIA': {'country_code': 'NA'}, - 'NAURU': {'country_code': 'NR'}, - 'NEPAL': {'country_code': 'NP'}, - 'NETHERLANDS': {'country_code': 'NL'}, - 'NEW_CALEDONIA': {'country_code': 'NC'}, - 'NEW_ZEALAND': {'country_code': 'NZ'}, - 'NICARAGUA': {'country_code': 'NI'}, - 'NIGER': {'country_code': 'NE'}, - 'NIGERIA': {'country_code': 'NG'}, - 'NIUE': {'country_code': 'NU'}, - 'NORFOLK_ISLAND': {'country_code': 'NF'}, - 'NORTHERN_MARIANA_ISLANDS': {'country_code': 'MP'}, - 'NORWAY': {'country_code': 'NO'}, - 'OMAN': {'country_code': 'OM'}, - 'PAKISTAN': {'country_code': 'PK'}, - 'PALAU': {'country_code': 'PW'}, - 'PALESTINE': {'country_code': 'PS'}, - 'PANAMA': {'country_code': 'PA'}, - 'PAPUA_NEW_GUINEA': {'country_code': 'PG'}, - 'PARAGUAY': {'country_code': 'PY'}, - 'PERU': {'country_code': 'PE'}, - 'PHILIPPINES': {'country_code': 'PH'}, - 'PITCAIRN': {'country_code': 'PN'}, - 'POLAND': {'country_code': 'PL'}, - 'PORTUGAL': {'country_code': 'PT'}, - 'PUERTO_RICO': {'country_code': 'PR'}, - 'QATAR': {'country_code': 'QA'}, - 'RÉUNION': {'country_code': 'RE'}, - 'ROMANIA': {'country_code': 'RO'}, - 'RUSSIAN_FEDERATION': {'country_code': 'RU'}, - 'RWANDA': {'country_code': 'RW'}, - 'SAINT_BARTHELEMY': {'country_code': 'BL'}, - 'SAINT_KITTS_AND_NEVIS': {'country_code': 'KN'}, - 'SAINT_LUCIA': {'country_code': 'LC'}, - 'SAINT_MARTIN': {'country_code': 'MF'}, - 'SAINT_PIERRE_AND_MIQUELON': {'country_code': 'PM'}, - 'SAINT_VINCENT_AND_THE_GRENADINES': {'country_code': 'VC'}, - 'SAMOA': {'country_code': 'WS'}, - 'SAN_MARINO': {'country_code': 'SM'}, - 'SAO_TOME_AND_PRINCIPE': {'country_code': 'ST'}, - 'SAUDI_ARABIA': {'country_code': 'SA'}, - 'SENEGAL': {'country_code': 'SN'}, - 'SERBIA': {'country_code': 'RS'}, - 'SEYCHELLES': {'country_code': 'SC'}, - 'SIERRA_LEONE': {'country_code': 'SL'}, - 'SINGAPORE': {'country_code': 'SG'}, - 'SINT_MAARTEN': {'country_code': 'SX'}, - 'SLOVAKIA': {'country_code': 'SK'}, - 'SLOVENIA': {'country_code': 'SI'}, - 'SOLOMON_ISLANDS': {'country_code': 'SB'}, - 'SOMALIA': {'country_code': 'SO'}, - 'SOUTH_AFRICA': {'country_code': 'ZA'}, - 'SOUTH_GEORGIA': {'country_code': 'GS'}, - 'SOUTH_SUDAN': {'country_code': 'SS'}, - 'SPAIN': {'country_code': 'ES'}, - 'SRI_LANKA': {'country_code': 'LK'}, - 'SUDAN': {'country_code': 'SD'}, - 'SURINAME': {'country_code': 'SR'}, - 'SVALBARD_AND_JAN_MAYEN': {'country_code': 'SJ'}, - 'SWAZILAND': {'country_code': 'SZ'}, - 'SWEDEN': {'country_code': 'SE'}, - 'SWITZERLAND': {'country_code': 'CH'}, - 'SYRIAN_ARAB_REPUBLIC': {'country_code': 'SY'}, - 'TAIWAN': {'country_code': 'TW'}, - 'TAJIKISTAN': {'country_code': 'TJ'}, - 'TANZANIA': {'country_code': 'TZ'}, - 'THAILAND': {'country_code': 'TH'}, - 'TIMOR-LESTE': {'country_code': 'TL'}, - 'TOGO': {'country_code': 'TG'}, - 'TOKELAU': {'country_code': 'TK'}, - 'TONGA': {'country_code': 'TO'}, - 'TRINIDAD_AND_TOBAGO': {'country_code': 'TT'}, - 'TUNISIA': {'country_code': 'TN'}, - 'TURKEY': {'country_code': 'TR'}, - 'TURKMENISTAN': {'country_code': 'TM'}, - 'TURKS_AND_CAICOS_ISLANDS': {'country_code': 'TC'}, - 'TUVALU': {'country_code': 'TV'}, - 'UGANDA': {'country_code': 'UG'}, - 'UKRAINE': {'country_code': 'UA'}, - 'UNITED_ARAB_EMIRATES': {'country_code': 'AE'}, - 'UNITED_KINGDOM': {'country_code': 'GB'}, - 'UNITED_STATES': {'country_code': 'US'}, - 'UNITED_STATES_MINOR_OUTLYING_ISLANDS': {'country_code': 'UM'}, - 'URUGUAY': {'country_code': 'UY'}, - 'UZBEKISTAN': {'country_code': 'UZ'}, - 'VANUATU': {'country_code': 'VU'}, - 'VENEZUELA': {'country_code': 'VE'}, - 'VIETNAM': {'country_code': 'VN'}, - 'VIRGIN_ISLANDS_BRITISH': {'country_code': 'VG'}, - 'VIRGIN_ISLANDS_US': {'country_code': 'VI'}, - 'WALLIS_AND_FUTUNA': {'country_code': 'WF'}, - 'WESTERN_SAHARA': {'country_code': 'EH'}, - 'YEMEN': {'country_code': 'YE'}, - 'ZAMBIA': {'country_code': 'ZM'}, - 'ZIMBABWE': {'country_code': 'ZW'}, - 'NON_COUNTRY': {'country_code': 'XX'} + 'AFGHANISTAN': { + 'country_code': 'AF' + }, + 'ALAND_ISLANDS': { + 'country_code': 'AX' + }, + 'ALBANIA': { + 'country_code': 'AL' + }, + 'ALGERIA': { + 'country_code': 'DZ' + }, + 'AMERICAN_SAMOA': { + 'country_code': 'AS' + }, + 'ANDORRA': { + 'country_code': 'AD' + }, + 'ANGOLA': { + 'country_code': 'AO' + }, + 'ANGUILLA': { + 'country_code': 'AI' + }, + 'ANTARCTICA': { + 'country_code': 'AQ' + }, + 'ANTIGUA_AND_BARBUDA': { + 'country_code': 'AG' + }, + 'ARGENTINA': { + 'country_code': 'AR' + }, + 'ARMENIA': { + 'country_code': 'AM' + }, + 'ARUBA': { + 'country_code': 'AW' + }, + 'AUSTRALIA': { + 'country_code': 'AU' + }, + 'AUSTRIA': { + 'country_code': 'AT' + }, + 'AZERBAIJAN': { + 'country_code': 'AZ' + }, + 'BAHAMAS': { + 'country_code': 'BS' + }, + 'BAHRAIN': { + 'country_code': 'BH' + }, + 'BANGLADESH': { + 'country_code': 'BD' + }, + 'BARBADOS': { + 'country_code': 'BB' + }, + 'BELARUS': { + 'country_code': 'BY' + }, + 'BELGIUM': { + 'country_code': 'BE' + }, + 'BELIZE': { + 'country_code': 'BZ' + }, + 'BENIN': { + 'country_code': 'BJ' + }, + 'BERMUDA': { + 'country_code': 'BM' + }, + 'BHUTAN': { + 'country_code': 'BT' + }, + 'BOLIVIA': { + 'country_code': 'BO' + }, + 'BONAIRE': { + 'country_code': 'BQ' + }, + 'BOSNIA_AND_HERZEGOVINA': { + 'country_code': 'BA' + }, + 'BOTSWANA': { + 'country_code': 'BW' + }, + 'BOUVET_ISLAND': { + 'country_code': 'BV' + }, + 'BRAZIL': { + 'country_code': 'BR' + }, + 'BRITISH_INDIAN_OCEAN_TERRITORY': { + 'country_code': 'IO' + }, + 'BRUNEI_DARUSSALAM': { + 'country_code': 'BN' + }, + 'BULGARIA': { + 'country_code': 'BG' + }, + 'BURKINA_FASO': { + 'country_code': 'BF' + }, + 'BURUNDI': { + 'country_code': 'BI' + }, + 'CAMBODIA': { + 'country_code': 'KH' + }, + 'CAMEROON': { + 'country_code': 'CM' + }, + 'CANADA': { + 'country_code': 'CA' + }, + 'CAPE_VERDE': { + 'country_code': 'CV' + }, + 'CAYMAN_ISLANDS': { + 'country_code': 'KY' + }, + 'CENTRAL_AFRICAN_REPUBLIC': { + 'country_code': 'CF' + }, + 'CHAD': { + 'country_code': 'TD' + }, + 'CHILE': { + 'country_code': 'CL' + }, + 'CHINA': { + 'country_code': 'CN' + }, + 'CHRISTMAS_ISLAND': { + 'country_code': 'CX' + }, + 'COCOS_ISLANDS': { + 'country_code': 'CC' + }, + 'COLOMBIA': { + 'country_code': 'CO' + }, + 'COMOROS': { + 'country_code': 'KM' + }, + 'CONGO': { + 'country_code': 'CG' + }, + 'DEMOCRATIC_REPUBLIC_CONGO': { + 'country_code': 'CD' + }, + 'COOK_ISLANDS': { + 'country_code': 'CK' + }, + 'COSTA_RICA': { + 'country_code': 'CR' + }, + 'COTE_D_IVOIRE': { + 'country_code': 'CI' + }, + 'CROATIA': { + 'country_code': 'HR' + }, + 'CUBA': { + 'country_code': 'CU' + }, + 'CURACAO': { + 'country_code': 'CW' + }, + 'CYPRUS': { + 'country_code': 'CY' + }, + 'CZECH_REPUBLIC': { + 'country_code': 'CZ' + }, + 'DENMARK': { + 'country_code': 'DK' + }, + 'DJIBOUTI': { + 'country_code': 'DJ' + }, + 'DOMINICA': { + 'country_code': 'DM' + }, + 'DOMINICAN_REPUBLIC': { + 'country_code': 'DO' + }, + 'ECUADOR': { + 'country_code': 'EC' + }, + 'EGYPT': { + 'country_code': 'EG' + }, + 'EL_SALVADOR': { + 'country_code': 'SV' + }, + 'EQUATORIAL_GUINEA': { + 'country_code': 'GQ' + }, + 'ERITREA': { + 'country_code': 'ER' + }, + 'ESTONIA': { + 'country_code': 'EE' + }, + 'ETHIOPIA': { + 'country_code': 'ET' + }, + 'FALKLAND_ISLANDS_(MALVINAS)': { + 'country_code': 'FK' + }, + 'FAROE_ISLANDS': { + 'country_code': 'FO' + }, + 'FIJI': { + 'country_code': 'FJ' + }, + 'FINLAND': { + 'country_code': 'FI' + }, + 'FRANCE': { + 'country_code': 'FR' + }, + 'FRENCH_GUIANA': { + 'country_code': 'GF' + }, + 'FRENCH_POLYNESIA': { + 'country_code': 'PF' + }, + 'FRENCH_SOUTHERN_TERRITORIES': { + 'country_code': 'TF' + }, + 'GABON': { + 'country_code': 'GA' + }, + 'GAMBIA': { + 'country_code': 'GM' + }, + 'GEORGIA': { + 'country_code': 'GE' + }, + 'GERMANY': { + 'country_code': 'DE' + }, + 'GHANA': { + 'country_code': 'GH' + }, + 'GIBRALTAR': { + 'country_code': 'GI' + }, + 'GREECE': { + 'country_code': 'GR' + }, + 'GREENLAND': { + 'country_code': 'GL' + }, + 'GRENADA': { + 'country_code': 'GD' + }, + 'GUADELOUPE': { + 'country_code': 'GP' + }, + 'GUAM': { + 'country_code': 'GU' + }, + 'GUATEMALA': { + 'country_code': 'GT' + }, + 'GUERNSEY': { + 'country_code': 'GG' + }, + 'GUINEA': { + 'country_code': 'GN' + }, + 'GUINEA-BISSAU': { + 'country_code': 'GW' + }, + 'GUYANA': { + 'country_code': 'GY' + }, + 'HAITI': { + 'country_code': 'HT' + }, + 'HEARD_ISLAND_AND_MCDONALD_ISLANDS': { + 'country_code': 'HM' + }, + 'VATICAN_CITY_STATE': { + 'country_code': 'VA' + }, + 'HONDURAS': { + 'country_code': 'HN' + }, + 'HONG_KONG': { + 'country_code': 'HK' + }, + 'HUNGARY': { + 'country_code': 'HU' + }, + 'ICELAND': { + 'country_code': 'IS' + }, + 'INDIA': { + 'country_code': 'IN' + }, + 'INDONESIA': { + 'country_code': 'ID' + }, + 'IRAN': { + 'country_code': 'IR' + }, + 'IRAQ': { + 'country_code': 'IQ' + }, + 'IRELAND': { + 'country_code': 'IE' + }, + 'ISLE_OF_MAN': { + 'country_code': 'IM' + }, + 'ISRAEL': { + 'country_code': 'IL' + }, + 'ITALY': { + 'country_code': 'IT' + }, + 'JAMAICA': { + 'country_code': 'JM' + }, + 'JAPAN': { + 'country_code': 'JP' + }, + 'JERSEY': { + 'country_code': 'JE' + }, + 'JORDAN': { + 'country_code': 'JO' + }, + 'KAZAKHSTAN': { + 'country_code': 'KZ' + }, + 'KENYA': { + 'country_code': 'KE' + }, + 'KIRIBATI': { + 'country_code': 'KI' + }, + 'DEMOCRATIC_PEOPLE_S_REPUBLIC_OF_KOREA': { + 'country_code': 'KP' + }, + 'REPUBLIC_OF_KOREA': { + 'country_code': 'KR' + }, + 'KUWAIT': { + 'country_code': 'KW' + }, + 'KYRGYZSTAN': { + 'country_code': 'KG' + }, + 'LAO': { + 'country_code': 'LA' + }, + 'LATVIA': { + 'country_code': 'LV' + }, + 'LEBANON': { + 'country_code': 'LB' + }, + 'LESOTHO': { + 'country_code': 'LS' + }, + 'LIBERIA': { + 'country_code': 'LR' + }, + 'LIBYA': { + 'country_code': 'LY' + }, + 'LIECHTENSTEIN': { + 'country_code': 'LI' + }, + 'LITHUANIA': { + 'country_code': 'LT' + }, + 'LUXEMBOURG': { + 'country_code': 'LU' + }, + 'MACAO': { + 'country_code': 'MO' + }, + 'MACEDONIA': { + 'country_code': 'MK' + }, + 'MADAGASCAR': { + 'country_code': 'MG' + }, + 'MALAWI': { + 'country_code': 'MW' + }, + 'MALAYSIA': { + 'country_code': 'MY' + }, + 'MALDIVES': { + 'country_code': 'MV' + }, + 'MALI': { + 'country_code': 'ML' + }, + 'MALTA': { + 'country_code': 'MT' + }, + 'MARSHALL_ISLANDS': { + 'country_code': 'MH' + }, + 'MARTINIQUE': { + 'country_code': 'MQ' + }, + 'MAURITANIA': { + 'country_code': 'MR' + }, + 'MAURITIUS': { + 'country_code': 'MU' + }, + 'MAYOTTE': { + 'country_code': 'YT' + }, + 'MEXICO': { + 'country_code': 'MX' + }, + 'MICRONESIA': { + 'country_code': 'FM' + }, + 'MOLDOVA': { + 'country_code': 'MD' + }, + 'MONACO': { + 'country_code': 'MC' + }, + 'MONGOLIA': { + 'country_code': 'MN' + }, + 'MONTENEGRO': { + 'country_code': 'ME' + }, + 'MONTSERRAT': { + 'country_code': 'MS' + }, + 'MOROCCO': { + 'country_code': 'MA' + }, + 'MOZAMBIQUE': { + 'country_code': 'MZ' + }, + 'MYANMAR': { + 'country_code': 'MM' + }, + 'NAMIBIA': { + 'country_code': 'NA' + }, + 'NAURU': { + 'country_code': 'NR' + }, + 'NEPAL': { + 'country_code': 'NP' + }, + 'NETHERLANDS': { + 'country_code': 'NL' + }, + 'NEW_CALEDONIA': { + 'country_code': 'NC' + }, + 'NEW_ZEALAND': { + 'country_code': 'NZ' + }, + 'NICARAGUA': { + 'country_code': 'NI' + }, + 'NIGER': { + 'country_code': 'NE' + }, + 'NIGERIA': { + 'country_code': 'NG' + }, + 'NIUE': { + 'country_code': 'NU' + }, + 'NORFOLK_ISLAND': { + 'country_code': 'NF' + }, + 'NORTHERN_MARIANA_ISLANDS': { + 'country_code': 'MP' + }, + 'NORWAY': { + 'country_code': 'NO' + }, + 'OMAN': { + 'country_code': 'OM' + }, + 'PAKISTAN': { + 'country_code': 'PK' + }, + 'PALAU': { + 'country_code': 'PW' + }, + 'PALESTINE': { + 'country_code': 'PS' + }, + 'PANAMA': { + 'country_code': 'PA' + }, + 'PAPUA_NEW_GUINEA': { + 'country_code': 'PG' + }, + 'PARAGUAY': { + 'country_code': 'PY' + }, + 'PERU': { + 'country_code': 'PE' + }, + 'PHILIPPINES': { + 'country_code': 'PH' + }, + 'PITCAIRN': { + 'country_code': 'PN' + }, + 'POLAND': { + 'country_code': 'PL' + }, + 'PORTUGAL': { + 'country_code': 'PT' + }, + 'PUERTO_RICO': { + 'country_code': 'PR' + }, + 'QATAR': { + 'country_code': 'QA' + }, + 'RÉUNION': { + 'country_code': 'RE' + }, + 'ROMANIA': { + 'country_code': 'RO' + }, + 'RUSSIAN_FEDERATION': { + 'country_code': 'RU' + }, + 'RWANDA': { + 'country_code': 'RW' + }, + 'SAINT_BARTHELEMY': { + 'country_code': 'BL' + }, + 'SAINT_KITTS_AND_NEVIS': { + 'country_code': 'KN' + }, + 'SAINT_LUCIA': { + 'country_code': 'LC' + }, + 'SAINT_MARTIN': { + 'country_code': 'MF' + }, + 'SAINT_PIERRE_AND_MIQUELON': { + 'country_code': 'PM' + }, + 'SAINT_VINCENT_AND_THE_GRENADINES': { + 'country_code': 'VC' + }, + 'SAMOA': { + 'country_code': 'WS' + }, + 'SAN_MARINO': { + 'country_code': 'SM' + }, + 'SAO_TOME_AND_PRINCIPE': { + 'country_code': 'ST' + }, + 'SAUDI_ARABIA': { + 'country_code': 'SA' + }, + 'SENEGAL': { + 'country_code': 'SN' + }, + 'SERBIA': { + 'country_code': 'RS' + }, + 'SEYCHELLES': { + 'country_code': 'SC' + }, + 'SIERRA_LEONE': { + 'country_code': 'SL' + }, + 'SINGAPORE': { + 'country_code': 'SG' + }, + 'SINT_MAARTEN': { + 'country_code': 'SX' + }, + 'SLOVAKIA': { + 'country_code': 'SK' + }, + 'SLOVENIA': { + 'country_code': 'SI' + }, + 'SOLOMON_ISLANDS': { + 'country_code': 'SB' + }, + 'SOMALIA': { + 'country_code': 'SO' + }, + 'SOUTH_AFRICA': { + 'country_code': 'ZA' + }, + 'SOUTH_GEORGIA': { + 'country_code': 'GS' + }, + 'SOUTH_SUDAN': { + 'country_code': 'SS' + }, + 'SPAIN': { + 'country_code': 'ES' + }, + 'SRI_LANKA': { + 'country_code': 'LK' + }, + 'SUDAN': { + 'country_code': 'SD' + }, + 'SURINAME': { + 'country_code': 'SR' + }, + 'SVALBARD_AND_JAN_MAYEN': { + 'country_code': 'SJ' + }, + 'SWAZILAND': { + 'country_code': 'SZ' + }, + 'SWEDEN': { + 'country_code': 'SE' + }, + 'SWITZERLAND': { + 'country_code': 'CH' + }, + 'SYRIAN_ARAB_REPUBLIC': { + 'country_code': 'SY' + }, + 'TAIWAN': { + 'country_code': 'TW' + }, + 'TAJIKISTAN': { + 'country_code': 'TJ' + }, + 'TANZANIA': { + 'country_code': 'TZ' + }, + 'THAILAND': { + 'country_code': 'TH' + }, + 'TIMOR-LESTE': { + 'country_code': 'TL' + }, + 'TOGO': { + 'country_code': 'TG' + }, + 'TOKELAU': { + 'country_code': 'TK' + }, + 'TONGA': { + 'country_code': 'TO' + }, + 'TRINIDAD_AND_TOBAGO': { + 'country_code': 'TT' + }, + 'TUNISIA': { + 'country_code': 'TN' + }, + 'TURKEY': { + 'country_code': 'TR' + }, + 'TURKMENISTAN': { + 'country_code': 'TM' + }, + 'TURKS_AND_CAICOS_ISLANDS': { + 'country_code': 'TC' + }, + 'TUVALU': { + 'country_code': 'TV' + }, + 'UGANDA': { + 'country_code': 'UG' + }, + 'UKRAINE': { + 'country_code': 'UA' + }, + 'UNITED_ARAB_EMIRATES': { + 'country_code': 'AE' + }, + 'UNITED_KINGDOM': { + 'country_code': 'GB' + }, + 'UNITED_STATES': { + 'country_code': 'US' + }, + 'UNITED_STATES_MINOR_OUTLYING_ISLANDS': { + 'country_code': 'UM' + }, + 'URUGUAY': { + 'country_code': 'UY' + }, + 'UZBEKISTAN': { + 'country_code': 'UZ' + }, + 'VANUATU': { + 'country_code': 'VU' + }, + 'VENEZUELA': { + 'country_code': 'VE' + }, + 'VIETNAM': { + 'country_code': 'VN' + }, + 'VIRGIN_ISLANDS_BRITISH': { + 'country_code': 'VG' + }, + 'VIRGIN_ISLANDS_US': { + 'country_code': 'VI' + }, + 'WALLIS_AND_FUTUNA': { + 'country_code': 'WF' + }, + 'WESTERN_SAHARA': { + 'country_code': 'EH' + }, + 'YEMEN': { + 'country_code': 'YE' + }, + 'ZAMBIA': { + 'country_code': 'ZM' + }, + 'ZIMBABWE': { + 'country_code': 'ZW' + }, + 'NON_COUNTRY': { + 'country_code': 'XX' + } } diff --git a/acts/framework/acts/controllers/ap_lib/third_party_ap_profiles/actiontec.py b/acts/framework/acts/controllers/ap_lib/third_party_ap_profiles/actiontec.py index e2452d63d1..1e576ebded 100644 --- a/acts/framework/acts/controllers/ap_lib/third_party_ap_profiles/actiontec.py +++ b/acts/framework/acts/controllers/ap_lib/third_party_ap_profiles/actiontec.py @@ -98,7 +98,6 @@ def actiontec_pk5000(iface_wlan_2g=None, def actiontec_mi424wr(iface_wlan_2g=None, - iface_wlan_5g=None, channel=None, security=None, ssid=None): @@ -106,7 +105,6 @@ def actiontec_mi424wr(iface_wlan_2g=None, """A simulated implementation of an Actiontec MI424WR AP. Args: iface_wlan_2g: The 2.4Ghz interface of the test AP. - iface_wlan_5g: The 5Ghz interface of the test AP. channel: What channel to use (2.4Ghz or 5Ghz). security: A security profile. ssid: The network name. @@ -114,37 +112,34 @@ def actiontec_mi424wr(iface_wlan_2g=None, A hostapd config. Differences from real MI424WR: - ERP Information: + HT Capabilities: MI424WR: - Use Protection: Set + HT Rx STBC: Support for 1, 2, and 3 Simulated: - Use Protection: Not Set - HT Capabilities: - MI424WR: [TX-STBC][DSSS_CCK-40][RX-STBC123] - Simulated: [TX-STBC][DSSS_CCK-40][RX-STBC1] + HT Rx STBC: Support for 1 HT Information: MI424WR: RIFS: Premitted - Reserved (Subset 2): 0x1 Simulated: RIFS: Prohibited - Reserved (Subset 2): 0x0 """ - if not iface_wlan_2g or not iface_wlan_5g: - raise ValueError('WLAN interface for 2G and/or 5G is missing.') - - if (iface_wlan_2g not in hostapd_constants.INTERFACE_2G_LIST - or iface_wlan_5g not in hostapd_constants.INTERFACE_5G_LIST): + if channel > 11: + raise ValueError('The Actiontec MI424WR does not support 5Ghz. ' + 'Invalid channel (%s)' % channel) + if (iface_wlan_2g not in hostapd_constants.INTERFACE_2G_LIST): raise ValueError('Invalid interface name was passed.') - rates = {'supported_rates': '10 20 55 110 60 90 120 180 240 360 480 540'} - - if channel <= 11: - interface = iface_wlan_2g - rates['basic_rates'] = '10 20 55 110' - else: - interface = iface_wlan_5g - rates['basic_rates'] = '60 120 240' + if security: + if security.security_mode is hostapd_constants.WPA2: + if not security.wpa2_cipher == 'CCMP': + raise ValueError('The mock Actiontec MI424WR only supports a ' + 'WPA2 unicast and multicast cipher of CCMP.' + 'Invalid cipher mode (%s)' % + security.security.wpa2_cipher) + else: + raise ValueError('The mock Actiontec MI424WR only supports WPA2. ' + 'Invalid security mode (%s)' % + security.security_mode) n_capabilities = [ hostapd_constants.N_CAPABILITY_TX_STBC, @@ -152,6 +147,11 @@ def actiontec_mi424wr(iface_wlan_2g=None, hostapd_constants.N_CAPABILITY_RX_STBC1 ] + rates = { + 'basic_rates': '10 20 55 110', + 'supported_rates': '10 20 55 110 60 90 120 180 240 360 480 540' + } + # Proprietary Atheros Communication: Adv Capability IE # Proprietary Atheros Communication: Unknown IE # Country Info: US Only IE @@ -161,26 +161,15 @@ def actiontec_mi424wr(iface_wlan_2g=None, 'dd0a00037f04010000000000' '0706555320010b1b' } - additional_params = _merge_dicts(rates, vendor_elements) - if security: - if security.security_mode is hostapd_constants.WPA2: - if not security.wpa2_cipher == 'CCMP': - raise ValueError('The mock Actiontec MI424WR only supports a ' - 'WPA2 unicast and multicast cipher of CCMP.' - 'Invalid cipher mode (%s)' % - security.security.wpa2_cipher) - else: - raise ValueError('The mock Actiontec MI424WR only supports WPA2. ' - 'Invalid security mode (%s)' % - security.security_mode) + additional_params = _merge_dicts(rates, vendor_elements) config = hostapd_config.HostapdConfig( ssid=ssid, channel=channel, hidden=False, security=security, - interface=interface, + interface=iface_wlan_2g, mode=hostapd_constants.MODE_11N_MIXED, force_wmm=True, beacon_interval=100, diff --git a/acts/framework/acts/controllers/ap_lib/third_party_ap_profiles/asus.py b/acts/framework/acts/controllers/ap_lib/third_party_ap_profiles/asus.py new file mode 100644 index 0000000000..8ab68c5563 --- /dev/null +++ b/acts/framework/acts/controllers/ap_lib/third_party_ap_profiles/asus.py @@ -0,0 +1,395 @@ +# Copyright 2019 - The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from acts.controllers.ap_lib import hostapd_config +from acts.controllers.ap_lib import hostapd_constants + + +def _merge_dicts(*dict_args): + result = {} + for dictionary in dict_args: + result.update(dictionary) + return result + + +def asus_rtac66u(iface_wlan_2g=None, + iface_wlan_5g=None, + channel=None, + security=None, + ssid=None): + # TODO(b/143104825): Permit RIFS once it is supported + """A simulated implementation of an Asus RTAC66U AP. + Args: + iface_wlan_2g: The 2.4Ghz interface of the test AP. + iface_wlan_5g: The 5Ghz interface of the test AP. + channel: What channel to use. + security: A security profile. Must be none or WPA2 as this is what is + supported by the RTAC66U. + ssid: Network name + Returns: + A hostapd config + Differences from real RTAC66U: + 2.4 GHz: + Rates: + RTAC66U: + Supported: 1, 2, 5.5, 11, 18, 24, 36, 54 + Extended: 6, 9, 12, 48 + Simulated: + Supported: 1, 2, 5.5, 11, 6, 9, 12, 18 + Extended: 24, 36, 48, 54 + HT Capab: + Info + RTAC66U: Green Field supported + Simulated: Green Field not supported by driver + 5GHz: + VHT Capab: + RTAC66U: + SU Beamformer Supported, + SU Beamformee Supported, + Beamformee STS Capability: 3, + Number of Sounding Dimensions: 3, + VHT Link Adaptation: Both + Simulated: + Above are not supported by driver + VHT Operation Info: + RTAC66U: Basic MCS Map (0x0000) + Simulated: Basic MCS Map (0xfffc) + VHT Tx Power Envelope: + RTAC66U: Local Max Tx Pwr Constraint: 1.0 dBm + Simulated: Local Max Tx Pwr Constraint: 23.0 dBm + Both: + HT Capab: + A-MPDU + RTAC66U: MPDU Density 4 + Simulated: MPDU Density 8 + HT Info: + RTAC66U: RIFS Permitted + Simulated: RIFS Prohibited + """ + if not iface_wlan_2g or not iface_wlan_5g: + raise ValueError('Wlan interface for 2G and/or 5G is missing.') + if (iface_wlan_2g not in hostapd_constants.INTERFACE_2G_LIST + or iface_wlan_5g not in hostapd_constants.INTERFACE_5G_LIST): + raise ValueError('Invalid interface name was passed.') + if security: + if security.security_mode is hostapd_constants.WPA2: + if not security.wpa2_cipher == 'CCMP': + raise ValueError('The mock ASUS RT-AC66U only supports a WPA2 ' + 'unicast and multicast cipher of CCMP. ' + 'Invalid cipher mode (%s)' % + security.security.wpa2_cipher) + else: + raise ValueError( + 'The Asus RT-AC66U only supports WPA2 or open. Invalid ' + 'security mode (%s)' % security.security_mode) + + # Common Parameters + rates = {'supported_rates': '10 20 55 110 60 90 120 180 240 360 480 540'} + n_capabilities = [ + hostapd_constants.N_CAPABILITY_LDPC, + hostapd_constants.N_CAPABILITY_TX_STBC, + hostapd_constants.N_CAPABILITY_RX_STBC1, + hostapd_constants.N_CAPABILITY_MAX_AMSDU_7935, + hostapd_constants.N_CAPABILITY_DSSS_CCK_40, + hostapd_constants.N_CAPABILITY_SGI20 + ] + # WPS IE + # Broadcom IE + vendor_elements = { + 'vendor_elements': + 'dd310050f204104a00011010440001021047001093689729d373c26cb1563c6c570f33' + 'd7103c0001031049000600372a000120' + 'dd090010180200001c0000' + } + + # 2.4GHz + if channel <= 11: + interface = iface_wlan_2g + rates['basic_rates'] = '10 20 55 110' + mode = hostapd_constants.MODE_11N_MIXED + ac_capabilities = None + vht_channel_width = None + vht_center_channel = None + + # 5GHz + else: + interface = iface_wlan_5g + rates['basic_rates'] = '60 120 240' + mode = hostapd_constants.MODE_11AC_MIXED + ac_capabilities = [ + hostapd_constants.AC_CAPABILITY_RXLDPC, + hostapd_constants.AC_CAPABILITY_SHORT_GI_80, + hostapd_constants.AC_CAPABILITY_TX_STBC_2BY1, + hostapd_constants.AC_CAPABILITY_RX_STBC_1, + hostapd_constants.AC_CAPABILITY_MAX_MPDU_11454, + hostapd_constants.AC_CAPABILITY_MAX_A_MPDU_LEN_EXP7 + ] + vht_channel_width = 40 + vht_center_channel = 36 + + additional_params = _merge_dicts(rates, vendor_elements, + hostapd_constants.UAPSD_ENABLED) + + config = hostapd_config.HostapdConfig( + ssid=ssid, + channel=channel, + hidden=False, + security=security, + interface=interface, + mode=mode, + force_wmm=True, + beacon_interval=100, + dtim_period=3, + short_preamble=False, + n_capabilities=n_capabilities, + ac_capabilities=ac_capabilities, + vht_channel_width=vht_channel_width, + vht_center_channel=vht_center_channel, + additional_parameters=additional_params) + + return config + + +def asus_rtac86u(iface_wlan_2g=None, + iface_wlan_5g=None, + channel=None, + security=None, + ssid=None): + """A simulated implementation of an Asus RTAC86U AP. + Args: + iface_wlan_2g: The 2.4Ghz interface of the test AP. + iface_wlan_5g: The 5Ghz interface of the test AP. + channel: What channel to use. + security: A security profile. Must be none or WPA2 as this is what is + supported by the RTAC86U. + ssid: Network name + Returns: + A hostapd config + Differences from real RTAC86U: + 2.4GHz: + Rates: + RTAC86U: + Supported: 1, 2, 5.5, 11, 18, 24, 36, 54 + Extended: 6, 9, 12, 48 + Simulated: + Supported: 1, 2, 5.5, 11, 6, 9, 12, 18 + Extended: 24, 36, 48, 54 + 5GHz: + Country Code: + Simulated: Has two country code IEs, one that matches + the actual, and another explicit IE that was required for + hostapd's 802.11d to work. + Both (w/ WPA2): + RSN Capabilities: + RTA86U: 0x000c (RSN PTKSA Replay Counter Capab: 16) + Simulated: 0x0000 + """ + if not iface_wlan_2g or not iface_wlan_5g: + raise ValueError('Wlan interface for 2G and/or 5G is missing.') + if (iface_wlan_2g not in hostapd_constants.INTERFACE_2G_LIST + or iface_wlan_5g not in hostapd_constants.INTERFACE_5G_LIST): + raise ValueError('Invalid interface name was passed.') + if security: + if security.security_mode is hostapd_constants.WPA2: + if not security.wpa2_cipher == 'CCMP': + raise ValueError('The mock ASUS RTAC86U only supports a WPA2 ' + 'unicast and multicast cipher of CCMP. ' + 'Invalid cipher mode (%s)' % + security.security.wpa2_cipher) + else: + raise ValueError( + 'The Asus RTAC86U only supports WPA2 or open. Invalid ' + 'security mode (%s)' % security.security_mode) + + # Common Parameters + rates = {'supported_rates': '10 20 55 110 60 90 120 180 240 360 480 540'} + qbss = {'bss_load_update_period': 50, 'chan_util_avg_period': 600} + + # 2.4GHz + if channel <= 11: + interface = iface_wlan_2g + mode = hostapd_constants.MODE_11G + rates['basic_rates'] = '10 20 55 110' + spectrum_mgmt = False + # Measurement Pilot Transmission IE + vendor_elements = {'vendor_elements': '42020000'} + + # 5GHz + else: + interface = iface_wlan_5g + mode = hostapd_constants.MODE_11A + rates['basic_rates'] = '60 120 240' + spectrum_mgmt = True, + # Country Information IE (w/ individual channel info) + # TPC Report Transmit Power IE + # Measurement Pilot Transmission IE + vendor_elements = { + 'vendor_elements': + '074255532024011e28011e2c011e30011e34011e38011e3c011e40011e64011e' + '68011e6c011e70011e74011e84011e88011e8c011e95011e99011e9d011ea1011e' + 'a5011e' + '23021300' + '42020000' + } + + additional_params = _merge_dicts(rates, qbss, vendor_elements) + + config = hostapd_config.HostapdConfig( + ssid=ssid, + channel=channel, + hidden=False, + security=security, + interface=interface, + mode=mode, + force_wmm=False, + beacon_interval=100, + dtim_period=3, + short_preamble=False, + spectrum_mgmt_required=spectrum_mgmt, + additional_parameters=additional_params) + return config + + +def asus_rtac5300(iface_wlan_2g=None, + iface_wlan_5g=None, + channel=None, + security=None, + ssid=None): + # TODO(b/143104825): Permit RIFS once it is supported + """A simulated implementation of an Asus RTAC5300 AP. + Args: + iface_wlan_2g: The 2.4Ghz interface of the test AP. + iface_wlan_5g: The 5Ghz interface of the test AP. + channel: What channel to use. + security: A security profile. Must be none or WPA2 as this is what is + supported by the RTAC5300. + ssid: Network name + Returns: + A hostapd config + Differences from real RTAC5300: + 2.4GHz: + Rates: + RTAC86U: + Supported: 1, 2, 5.5, 11, 18, 24, 36, 54 + Extended: 6, 9, 12, 48 + Simulated: + Supported: 1, 2, 5.5, 11, 6, 9, 12, 18 + Extended: 24, 36, 48, 54 + 5GHz: + VHT Capab: + RTAC5300: + SU Beamformer Supported, + SU Beamformee Supported, + Beamformee STS Capability: 4, + Number of Sounding Dimensions: 4, + MU Beamformer Supported, + VHT Link Adaptation: Both + Simulated: + Above are not supported by driver + VHT Operation Info: + RTAC5300: Basic MCS Map (0x0000) + Simulated: Basic MCS Map (0xfffc) + VHT Tx Power Envelope: + RTAC5300: Local Max Tx Pwr Constraint: 1.0 dBm + Simulated: Local Max Tx Pwr Constraint: 23.0 dBm + Both: + HT Capab: + A-MPDU + RTAC5300: MPDU Density 4 + Simulated: MPDU Density 8 + HT Info: + RTAC5300: RIFS Permitted + Simulated: RIFS Prohibited + """ + if not iface_wlan_2g or not iface_wlan_5g: + raise ValueError('Wlan interface for 2G and/or 5G is missing.') + if (iface_wlan_2g not in hostapd_constants.INTERFACE_2G_LIST + or iface_wlan_5g not in hostapd_constants.INTERFACE_5G_LIST): + raise ValueError('Invalid interface name was passed.') + if security: + if security.security_mode is hostapd_constants.WPA2: + if not security.wpa2_cipher == 'CCMP': + raise ValueError('The mock ASUS RTAC5300 only supports a WPA2 ' + 'unicast and multicast cipher of CCMP. ' + 'Invalid cipher mode (%s)' % + security.security.wpa2_cipher) + else: + raise ValueError( + 'The Asus RTAC5300 only supports WPA2 or open. Invalid ' + 'security mode (%s)' % security.security_mode) + + # Common Parameters + rates = {'supported_rates': '10 20 55 110 60 90 120 180 240 360 480 540'} + qbss = {'bss_load_update_period': 50, 'chan_util_avg_period': 600} + n_capabilities = [ + hostapd_constants.N_CAPABILITY_LDPC, + hostapd_constants.N_CAPABILITY_TX_STBC, + hostapd_constants.N_CAPABILITY_RX_STBC1, + hostapd_constants.N_CAPABILITY_SGI20 + ] + # Broadcom IE + vendor_elements = {'vendor_elements': 'dd090010180200009c0000'} + + # 2.4GHz + if channel <= 11: + interface = iface_wlan_2g + rates['basic_rates'] = '10 20 55 110' + mode = hostapd_constants.MODE_11N_MIXED + # AsusTek IE + # Epigram 2.4GHz IE + vendor_elements['vendor_elements'] += 'dd25f832e4010101020100031411b5' \ + '2fd437509c30b3d7f5cf5754fb125aed3b8507045aed3b85' \ + 'dd1e00904c0418bf0cb2798b0faaff0000aaff0000c0050001000000c3020002' + ac_capabilities = None + vht_channel_width = None + vht_center_channel = None + + # 5GHz + else: + interface = iface_wlan_5g + rates['basic_rates'] = '60 120 240' + mode = hostapd_constants.MODE_11AC_MIXED + # Epigram 5GHz IE + vendor_elements['vendor_elements'] += 'dd0500904c0410' + ac_capabilities = [ + hostapd_constants.AC_CAPABILITY_RXLDPC, + hostapd_constants.AC_CAPABILITY_SHORT_GI_80, + hostapd_constants.AC_CAPABILITY_TX_STBC_2BY1, + hostapd_constants.AC_CAPABILITY_RX_STBC_1, + hostapd_constants.AC_CAPABILITY_MAX_MPDU_11454, + hostapd_constants.AC_CAPABILITY_MAX_A_MPDU_LEN_EXP7 + ] + vht_channel_width = 40 + vht_center_channel = 36 + + additional_params = _merge_dicts(rates, qbss, vendor_elements, + hostapd_constants.UAPSD_ENABLED) + + config = hostapd_config.HostapdConfig( + ssid=ssid, + channel=channel, + hidden=False, + security=security, + interface=interface, + mode=mode, + force_wmm=True, + beacon_interval=100, + dtim_period=3, + short_preamble=False, + n_capabilities=n_capabilities, + ac_capabilities=ac_capabilities, + vht_channel_width=vht_channel_width, + vht_center_channel=vht_center_channel, + additional_parameters=additional_params) + return config diff --git a/acts/framework/acts/controllers/ap_lib/third_party_ap_profiles/belkin.py b/acts/framework/acts/controllers/ap_lib/third_party_ap_profiles/belkin.py new file mode 100644 index 0000000000..00e80290b5 --- /dev/null +++ b/acts/framework/acts/controllers/ap_lib/third_party_ap_profiles/belkin.py @@ -0,0 +1,108 @@ +# Copyright 2019 - The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from acts.controllers.ap_lib import hostapd_config +from acts.controllers.ap_lib import hostapd_constants + + +def _merge_dicts(*dict_args): + result = {} + for dictionary in dict_args: + result.update(dictionary) + return result + + +def belkin_f9k1001v5(iface_wlan_2g=None, + channel=None, + security=None, + ssid=None): + # TODO(b/143104825): Permit RIFS once it is supported + """A simulated implementation of what a Belkin F9K1001v5 AP + Args: + iface_wlan_2g: The 2.4Ghz interface of the test AP. + channel: What channel to use. + security: A security profile (None or WPA2). + ssid: The network name. + Returns: + A hostapd config. + Differences from real F9K1001v5: + Rates: + F9K1001v5: + Supported: 1, 2, 5.5, 11, 18, 24, 36, 54 + Extended: 6, 9, 12, 48 + Simulated: + Supported: 1, 2, 5.5, 11, 6, 9, 12, 18 + Extended: 24, 36, 48, 54 + HT Info: + F9K1001v5: + RIFS: Permitted + Simulated: + RIFS: Prohibited + """ + if channel > 11: + raise ValueError('The Belkin F9k1001v5 does not support 5Ghz. ' + 'Invalid channel (%s)' % channel) + if (iface_wlan_2g not in hostapd_constants.INTERFACE_2G_LIST): + raise ValueError('Invalid interface name was passed.') + + if security: + if security.security_mode is hostapd_constants.WPA2: + if not security.wpa2_cipher == 'CCMP': + raise ValueError('The mock Belkin F9k1001v5 only supports a ' + 'WPA2 unicast and multicast cipher of CCMP.' + 'Invalid cipher mode (%s)' % + security.security.wpa2_cipher) + else: + raise ValueError('The mock Belkin F9k1001v5 only supports WPA2. ' + 'Invalid security mode (%s)' % + security.security_mode) + + n_capabilities = [ + hostapd_constants.N_CAPABILITY_SGI20, + hostapd_constants.N_CAPABILITY_SGI40, + hostapd_constants.N_CAPABILITY_TX_STBC, + hostapd_constants.N_CAPABILITY_MAX_AMSDU_7935, + hostapd_constants.N_CAPABILITY_DSSS_CCK_40 + ] + + rates = { + 'basic_rates': '10 20 55 110', + 'supported_rates': '10 20 55 110 60 90 120 180 240 360 480 540' + } + + # Broadcom IE + # WPS IE + vendor_elements = { + 'vendor_elements': + 'dd090010180200100c0000' + 'dd180050f204104a00011010440001021049000600372a000120' + } + + additional_params = _merge_dicts(rates, vendor_elements) + + config = hostapd_config.HostapdConfig( + ssid=ssid, + channel=channel, + hidden=False, + security=security, + interface=iface_wlan_2g, + mode=hostapd_constants.MODE_11N_MIXED, + force_wmm=True, + beacon_interval=100, + dtim_period=3, + short_preamble=False, + n_capabilities=n_capabilities, + additional_parameters=additional_params) + + return config diff --git a/acts/framework/acts/controllers/attenuator_lib/minicircuits/telnet.py b/acts/framework/acts/controllers/attenuator_lib/minicircuits/telnet.py index 96c890bd5f..be415d0ef3 100644 --- a/acts/framework/acts/controllers/attenuator_lib/minicircuits/telnet.py +++ b/acts/framework/acts/controllers/attenuator_lib/minicircuits/telnet.py @@ -37,11 +37,11 @@ class AttenuatorInstrument(attenuator.AttenuatorInstrument): the functionality of AttenuatorInstrument is contingent upon a telnet connection being established. """ - def __init__(self, num_atten=0): super(AttenuatorInstrument, self).__init__(num_atten) - self._tnhelper = _tnhelper._TNHelper( - tx_cmd_separator='\r\n', rx_cmd_separator='\r\n', prompt='') + self._tnhelper = _tnhelper._TNHelper(tx_cmd_separator='\r\n', + rx_cmd_separator='\r\n', + prompt='') def __del__(self): if self.is_open(): @@ -134,6 +134,9 @@ class AttenuatorInstrument(attenuator.AttenuatorInstrument): raise IndexError('Attenuator index out of range!', self.num_atten, idx) - atten_val_str = self._tnhelper.cmd('CHAN:%s:ATT?' % (idx + 1)) + if self.num_atten == 1: + atten_val_str = self._tnhelper.cmd(':ATT?') + else: + atten_val_str = self._tnhelper.cmd('CHAN:%s:ATT?' % (idx + 1)) atten_val = float(atten_val_str) return atten_val diff --git a/acts/framework/acts/controllers/cellular_simulator.py b/acts/framework/acts/controllers/cellular_simulator.py index a140c0f202..35f0885c46 100644 --- a/acts/framework/acts/controllers/cellular_simulator.py +++ b/acts/framework/acts/controllers/cellular_simulator.py @@ -133,6 +133,15 @@ class AbstractCellularSimulator: if config.tbs_pattern_on is not None: self.set_tbs_pattern_on(bts_index, config.tbs_pattern_on) + def set_lte_rrc_state_change_timer(self, enabled, time=10): + """ Configures the LTE RRC state change timer. + + Args: + enabled: a boolean indicating if the timer should be on or off. + time: time in seconds for the timer to expire + """ + raise NotImplementedError() + def set_band(self, bts_index, band): """ Sets the band for the indicated base station. diff --git a/acts/framework/acts/controllers/fuchsia_device.py b/acts/framework/acts/controllers/fuchsia_device.py index 350cfe4ee0..4e9b2dc9bd 100644 --- a/acts/framework/acts/controllers/fuchsia_device.py +++ b/acts/framework/acts/controllers/fuchsia_device.py @@ -535,7 +535,7 @@ class FuchsiaDevice: self.log.info(unable_to_connect_msg) raise e finally: - if action == 'stop': + if action == 'stop' and process_name == 'sl4f': self._persistent_ssh_conn.close() self._persistent_ssh_conn = None diff --git a/acts/framework/acts/controllers/fuchsia_lib/utils_lib.py b/acts/framework/acts/controllers/fuchsia_lib/utils_lib.py index 5df5b40127..f0b8dda4fb 100644 --- a/acts/framework/acts/controllers/fuchsia_lib/utils_lib.py +++ b/acts/framework/acts/controllers/fuchsia_lib/utils_lib.py @@ -30,21 +30,24 @@ def get_private_key(ip_address, ssh_config): Returns: The ssh private key """ + exceptions = [] try: logging.debug('Trying to load SSH key type: ed25519') return paramiko.ed25519key.Ed25519Key( filename=get_ssh_key_for_host(ip_address, ssh_config)) - except paramiko.SSHException: + except paramiko.SSHException as e: + exceptions.append(e) logging.debug('Failed loading SSH key type: ed25519') try: logging.debug('Trying to load SSH key type: rsa') return paramiko.RSAKey.from_private_key_file( filename=get_ssh_key_for_host(ip_address, ssh_config)) - except paramiko.SSHException: + except paramiko.SSHException as e: + exceptions.append(e) logging.debug('Failed loading SSH key type: rsa') - raise paramiko.SSHException('No valid ssh key type found') + raise Exception('No valid ssh key type found', exceptions) def create_ssh_connection(ip_address, diff --git a/acts/framework/acts/controllers/iperf_server.py b/acts/framework/acts/controllers/iperf_server.py index bedc05a748..039f143470 100755 --- a/acts/framework/acts/controllers/iperf_server.py +++ b/acts/framework/acts/controllers/iperf_server.py @@ -81,20 +81,25 @@ class IPerfResult(object): will be loaded and this funtion is not intended to be used with files containing multiple iperf client runs. """ - try: - with open(result_path, 'r') as f: - iperf_output = f.readlines() - if '}\n' in iperf_output: - iperf_output = iperf_output[:iperf_output.index('}\n') + 1] - iperf_string = ''.join(iperf_output) - iperf_string = iperf_string.replace('nan', '0') - self.result = json.loads(iperf_string) - except ValueError: - with open(result_path, 'r') as f: - # Possibly a result from interrupted iperf run, skip first line - # and try again. - lines = f.readlines()[1:] - self.result = json.loads(''.join(lines)) + # if result_path isn't a path, treat it as JSON + if not os.path.exists(result_path): + self.result = json.loads(result_path) + else: + try: + with open(result_path, 'r') as f: + iperf_output = f.readlines() + if '}\n' in iperf_output: + iperf_output = iperf_output[:iperf_output.index('}\n') + + 1] + iperf_string = ''.join(iperf_output) + iperf_string = iperf_string.replace('nan', '0') + self.result = json.loads(iperf_string) + except ValueError: + with open(result_path, 'r') as f: + # Possibly a result from interrupted iperf run, + # skip first line and try again. + lines = f.readlines()[1:] + self.result = json.loads(''.join(lines)) def _has_data(self): """Checks if the iperf result has valid throughput data. @@ -197,7 +202,7 @@ class IPerfResult(object): instantaneous_rates = self.instantaneous_rates[iperf_ignored_interval: -1] avg_rate = math.fsum(instantaneous_rates) / len(instantaneous_rates) - sqd_deviations = [(rate - avg_rate)**2 for rate in instantaneous_rates] + sqd_deviations = [(rate - avg_rate) ** 2 for rate in instantaneous_rates] std_dev = math.sqrt( math.fsum(sqd_deviations) / (len(sqd_deviations) - 1)) return std_dev diff --git a/acts/framework/acts/controllers/monsoon.py b/acts/framework/acts/controllers/monsoon.py index d5b24a2703..9488837dbb 100644 --- a/acts/framework/acts/controllers/monsoon.py +++ b/acts/framework/acts/controllers/monsoon.py @@ -13,1006 +13,28 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Interface for a USB-connected Monsoon power meter -(http://msoon.com/LabEquipment/PowerMonitor/). -Based on the original py2 script of kens@google.com -""" -import fcntl -import logging -import os -import select -import struct -import sys -import time -import collections +from acts.controllers.monsoon_lib.api.hvpm.monsoon import Monsoon as HvpmMonsoon +from acts.controllers.monsoon_lib.api.lvpm_stock.monsoon import Monsoon as LvpmStockMonsoon -# http://pyserial.sourceforge.net/ -# On ubuntu, apt-get install python3-pyserial -import serial - -import acts.signals - -from acts import utils -from acts.controllers import android_device - -ACTS_CONTROLLER_CONFIG_NAME = "Monsoon" -ACTS_CONTROLLER_REFERENCE_NAME = "monsoons" +ACTS_CONTROLLER_CONFIG_NAME = 'Monsoon' +ACTS_CONTROLLER_REFERENCE_NAME = 'monsoons' def create(configs): objs = [] - for c in configs: - objs.append(Monsoon(serial=int(c))) - return objs - - -def destroy(objs): - for obj in objs: - fcntl.flock(obj.mon._tempfile, fcntl.LOCK_UN) - obj.mon._tempfile.close() - - -class MonsoonError(acts.signals.ControllerError): - """Raised for exceptions encountered in monsoon lib.""" - - -class MonsoonProxy(object): - """Class that directly talks to monsoon over serial. - - Provides a simple class to use the power meter, e.g. - mon = monsoon.Monsoon() - mon.SetVoltage(3.7) - mon.StartDataCollection() - mydata = [] - while len(mydata) < 1000: - mydata.extend(mon.CollectData()) - mon.StopDataCollection() - - See http://wiki/Main/MonsoonProtocol for information on the protocol. - """ - - def __init__(self, device=None, serialno=None, wait=1): - """Establish a connection to a Monsoon. - - By default, opens the first available port, waiting if none are ready. - A particular port can be specified with "device", or a particular - Monsoon can be specified with "serialno" (using the number printed on - its back). With wait=0, IOError is thrown if a device is not - immediately available. - """ - self._coarse_ref = self._fine_ref = self._coarse_zero = 0 - self._fine_zero = self._coarse_scale = self._fine_scale = 0 - self._last_seq = 0 - self.start_voltage = 0 - self.serial = serialno - - if device: - self.ser = serial.Serial(device, timeout=1) - return - # Try all devices connected through USB virtual serial ports until we - # find one we can use. - while True: - for dev in os.listdir("/dev"): - prefix = "ttyACM" - # Prefix is different on Mac OS X. - if sys.platform == "darwin": - prefix = "tty.usbmodem" - if not dev.startswith(prefix): - continue - tmpname = "/tmp/monsoon.%s.%s" % (os.uname()[0], dev) - self._tempfile = open(tmpname, "w") - try: - os.chmod(tmpname, 0o666) - except OSError as e: - pass - - try: # use a lockfile to ensure exclusive access - fcntl.flock(self._tempfile, fcntl.LOCK_EX | fcntl.LOCK_NB) - except IOError as e: - logging.error("device %s is in use", dev) - continue - - try: # try to open the device - self.ser = serial.Serial("/dev/%s" % dev, timeout=1) - self.StopDataCollection() # just in case - self._FlushInput() # discard stale input - status = self.GetStatus() - except Exception as e: - logging.exception("Error opening device %s: %s", dev, e) - continue - - if not status: - logging.error("no response from device %s", dev) - elif serialno and status["serialNumber"] != serialno: - logging.error("Another device serial #%d seen on %s", - status["serialNumber"], dev) - else: - self.start_voltage = status["voltage1"] - return - - self._tempfile = None - if not wait: raise IOError("No device found") - logging.info("Waiting for device...") - time.sleep(1) - - def GetStatus(self): - """Requests and waits for status. - - Returns: - status dictionary. - """ - # status packet format - STATUS_FORMAT = ">BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH" - STATUS_FIELDS = [ - "packetType", - "firmwareVersion", - "protocolVersion", - "mainFineCurrent", - "usbFineCurrent", - "auxFineCurrent", - "voltage1", - "mainCoarseCurrent", - "usbCoarseCurrent", - "auxCoarseCurrent", - "voltage2", - "outputVoltageSetting", - "temperature", - "status", - "leds", - "mainFineResistor", - "serialNumber", - "sampleRate", - "dacCalLow", - "dacCalHigh", - "powerUpCurrentLimit", - "runTimeCurrentLimit", - "powerUpTime", - "usbFineResistor", - "auxFineResistor", - "initialUsbVoltage", - "initialAuxVoltage", - "hardwareRevision", - "temperatureLimit", - "usbPassthroughMode", - "mainCoarseResistor", - "usbCoarseResistor", - "auxCoarseResistor", - "defMainFineResistor", - "defUsbFineResistor", - "defAuxFineResistor", - "defMainCoarseResistor", - "defUsbCoarseResistor", - "defAuxCoarseResistor", - "eventCode", - "eventData", - ] - - self._SendStruct("BBB", 0x01, 0x00, 0x00) - while 1: # Keep reading, discarding non-status packets - read_bytes = self._ReadPacket() - if not read_bytes: - raise MonsoonError("Failed to read Monsoon status") - calsize = struct.calcsize(STATUS_FORMAT) - if len(read_bytes) != calsize or read_bytes[0] != 0x10: - raise MonsoonError( - "Wanted status, dropped type=0x%02x, len=%d", - read_bytes[0], len(read_bytes)) - status = dict( - zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT, read_bytes))) - p_type = status["packetType"] - if p_type != 0x10: - raise MonsoonError("Package type %s is not 0x10." % p_type) - for k in status.keys(): - if k.endswith("VoltageSetting"): - status[k] = 2.0 + status[k] * 0.01 - elif k.endswith("FineCurrent"): - pass # needs calibration data - elif k.endswith("CoarseCurrent"): - pass # needs calibration data - elif k.startswith("voltage") or k.endswith("Voltage"): - status[k] = status[k] * 0.000125 - elif k.endswith("Resistor"): - status[k] = 0.05 + status[k] * 0.0001 - if k.startswith("aux") or k.startswith("defAux"): - status[k] += 0.05 - elif k.endswith("CurrentLimit"): - status[k] = 8 * (1023 - status[k]) / 1023.0 - return status - - def RampVoltage(self, start, end): - v = start - if v < 3.0: v = 3.0 # protocol doesn't support lower than this - while (v < end): - self.SetVoltage(v) - v += .1 - time.sleep(.1) - self.SetVoltage(end) - - def SetVoltage(self, v): - """Set the output voltage, 0 to disable. - """ - if v == 0: - self._SendStruct("BBB", 0x01, 0x01, 0x00) - else: - self._SendStruct("BBB", 0x01, 0x01, int((v - 2.0) * 100)) - - def GetVoltage(self): - """Get the output voltage. - - Returns: - Current Output Voltage (in unit of v). - """ - try: - return self.GetStatus()["outputVoltageSetting"] - # Catch potential errors such as struct.error, TypeError and other - # unknown errors which would bring down the whole test - except Exception as e: - raise MonsoonError("Error getting Monsoon voltage") - - def SetMaxCurrent(self, i): - """Set the max output current. - """ - if i < 0 or i > 8: - raise MonsoonError(("Target max current %sA, is out of acceptable " - "range [0, 8].") % i) - val = 1023 - int((i / 8) * 1023) - self._SendStruct("BBB", 0x01, 0x0a, val & 0xff) - self._SendStruct("BBB", 0x01, 0x0b, val >> 8) - - def SetMaxPowerUpCurrent(self, i): - """Set the max power up current. - """ - if i < 0 or i > 8: - raise MonsoonError(("Target max current %sA, is out of acceptable " - "range [0, 8].") % i) - val = 1023 - int((i / 8) * 1023) - self._SendStruct("BBB", 0x01, 0x08, val & 0xff) - self._SendStruct("BBB", 0x01, 0x09, val >> 8) - - def SetUsbPassthrough(self, val): - """Set the USB passthrough mode: 0 = off, 1 = on, 2 = auto. - """ - self._SendStruct("BBB", 0x01, 0x10, val) - - def GetUsbPassthrough(self): - """Get the USB passthrough mode: 0 = off, 1 = on, 2 = auto. - - Returns: - Current USB passthrough mode. - """ - try: - return self.GetStatus()["usbPassthroughMode"] - # Catch potential errors such as struct.error, TypeError and other - # unknown errors which would bring down the whole test - except Exception as e: - raise MonsoonError("Error reading Monsoon USB passthrough status") - - def StartDataCollection(self): - """Tell the device to start collecting and sending measurement data. - """ - self._SendStruct("BBB", 0x01, 0x1b, 0x01) # Mystery command - self._SendStruct("BBBBBBB", 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8) - - def StopDataCollection(self): - """Tell the device to stop collecting measurement data. - """ - self._SendStruct("BB", 0x03, 0x00) # stop - - def CollectData(self): - """Return some current samples. Call StartDataCollection() first. - """ - while 1: # loop until we get data or a timeout - _bytes = self._ReadPacket() - if not _bytes: - raise MonsoonError("Data collection failed due to empty data") - if len(_bytes) < 4 + 8 + 1 or _bytes[0] < 0x20 or _bytes[0] > 0x2F: - logging.warning("Wanted data, dropped type=0x%02x, len=%d", - _bytes[0], len(_bytes)) - continue - - seq, _type, x, y = struct.unpack("BBBB", _bytes[:4]) - data = [ - struct.unpack(">hhhh", _bytes[x:x + 8]) - for x in range(4, - len(_bytes) - 8, 8) - ] - - if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF: - logging.warning("Data sequence skipped, lost packet?") - self._last_seq = seq - - if _type == 0: - if not self._coarse_scale or not self._fine_scale: - logging.warning( - "Waiting for calibration, dropped data packet.") - continue - out = [] - for main, usb, aux, voltage in data: - if main & 1: - coarse = ((main & ~1) - self._coarse_zero) - out.append(coarse * self._coarse_scale) - else: - out.append((main - self._fine_zero) * self._fine_scale) - return out - elif _type == 1: - self._fine_zero = data[0][0] - self._coarse_zero = data[1][0] - elif _type == 2: - self._fine_ref = data[0][0] - self._coarse_ref = data[1][0] - else: - logging.warning("Discarding data packet type=0x%02x", _type) - continue - - # See http://wiki/Main/MonsoonProtocol for details on these values. - if self._coarse_ref != self._coarse_zero: - self._coarse_scale = 2.88 / ( - self._coarse_ref - self._coarse_zero) - if self._fine_ref != self._fine_zero: - self._fine_scale = 0.0332 / (self._fine_ref - self._fine_zero) - - def _SendStruct(self, fmt, *args): - """Pack a struct (without length or checksum) and send it. - """ - # Flush out the input buffer before sending data - self._FlushInput() - data = struct.pack(fmt, *args) - data_len = len(data) + 1 - checksum = (data_len + sum(bytearray(data))) % 256 - out = struct.pack("B", data_len) + data + struct.pack("B", checksum) - self.ser.write(out) - - def _ReadPacket(self): - """Read a single data record as a string (without length or checksum). - """ - len_char = self.ser.read(1) - if not len_char: - raise MonsoonError("Reading from serial port timed out") - - data_len = ord(len_char) - if not data_len: - return "" - result = self.ser.read(int(data_len)) - result = bytearray(result) - if len(result) != data_len: - raise MonsoonError( - "Length mismatch, expected %d bytes, got %d bytes.", data_len, - len(result)) - body = result[:-1] - checksum = (sum(struct.unpack("B" * len(body), body)) + data_len) % 256 - if result[-1] != checksum: - raise MonsoonError( - "Invalid checksum from serial port! Expected %s, got %s", - hex(checksum), hex(result[-1])) - return result[:-1] - - def _FlushInput(self): - """ Flush all read data until no more available. """ - self.ser.reset_input_buffer() - flushed = 0 - while True: - ready_r, ready_w, ready_x = select.select([self.ser], [], - [self.ser], 0) - if len(ready_x) > 0: - raise MonsoonError("Exception from serial port.") - elif len(ready_r) > 0: - flushed += 1 - self.ser.read(1) # This may cause underlying buffering. - self.ser.reset_input_buffer( - ) # Flush the underlying buffer too. - else: - break - # if flushed > 0: - # logging.info("dropped >%d bytes" % flushed) - - -class MonsoonData(object): - """A class for reporting power measurement data from monsoon. - - Data means the measured current value in Amps. - """ - # Number of digits for long rounding. - lr = 8 - # Number of digits for short rounding - sr = 6 - # Delimiter for writing multiple MonsoonData objects to text file. - delimiter = "\n\n==========\n\n" - - def __init__(self, data_points, timestamps, hz, voltage, offset=0): - """Instantiates a MonsoonData object. - - Args: - data_points: A list of current values in Amp (float). - timestamps: A list of epoch timestamps (int). - hz: The hertz at which the data points are measured. - voltage: The voltage at which the data points are measured. - offset: The number of initial data points to discard - in calculations. - """ - self._data_points = data_points - self._timestamps = timestamps - self.offset = offset - num_of_data_pt = len(self._data_points) - if self.offset >= num_of_data_pt: - raise MonsoonError( - ("Offset number (%d) must be smaller than the " - "number of data points (%d).") % (offset, num_of_data_pt)) - self.data_points = self._data_points[self.offset:] - self.timestamps = self._timestamps[self.offset:] - self.hz = hz - self.voltage = voltage - self.tag = None - self._validate_data() - - @property - def average_current(self): - """Average current in the unit of mA. - """ - len_data_pt = len(self.data_points) - if len_data_pt == 0: - return 0 - cur = sum(self.data_points) * 1000 / len_data_pt - return round(cur, self.sr) - - @property - def total_charge(self): - """Total charged used in the unit of mAh. - """ - charge = (sum(self.data_points) / self.hz) * 1000 / 3600 - return round(charge, self.sr) - - @property - def total_power(self): - """Total power used. - """ - power = self.average_current * self.voltage - return round(power, self.sr) - - @staticmethod - def from_string(data_str): - """Creates a MonsoonData object from a string representation generated - by __str__. - - Args: - str: The string representation of a MonsoonData. - - Returns: - A MonsoonData object. - """ - lines = data_str.strip().split('\n') - err_msg = ("Invalid input string format. Is this string generated by " - "MonsoonData class?") - conditions = [ - len(lines) <= 4, "Average Current:" not in lines[1], - "Voltage: " not in lines[2], "Total Power: " not in lines[3], - "samples taken at " not in lines[4], - lines[5] != "Time" + ' ' * 7 + "Amp" - ] - if any(conditions): - raise MonsoonError(err_msg) - """Example string from Monsoon output file, first line is empty. - Line1: - Line2: test_2g_screenoff_dtimx2_marlin_OPD1.170706.006 - Line3: Average Current: 51.87984mA. - Line4: Voltage: 4.2V. - Line5: Total Power: 217.895328mW. - Line6: 150000 samples taken at 500Hz, with an offset of 0 samples. - """ - hz_str = lines[4].split()[4] - hz = int(hz_str[:-3]) - voltage_str = lines[2].split()[1] - voltage = float(voltage_str[:-2]) - lines = lines[6:] - t = [] - v = [] - for l in lines: - try: - timestamp, value = l.split(' ') - t.append(int(timestamp)) - v.append(float(value)) - except ValueError: - raise MonsoonError(err_msg) - return MonsoonData(v, t, hz, voltage) - - @staticmethod - def save_to_text_file(monsoon_data, file_path): - """Save multiple MonsoonData objects to a text file. - - Args: - monsoon_data: A list of MonsoonData objects to write to a text - file. - file_path: The full path of the file to save to, including the file - name. - """ - if not monsoon_data: - raise MonsoonError("Attempting to write empty Monsoon data to " - "file, abort") - utils.create_dir(os.path.dirname(file_path)) - with open(file_path, 'a') as f: - for md in monsoon_data: - f.write(str(md)) - f.write(MonsoonData.delimiter) - - @staticmethod - def from_text_file(file_path): - """Load MonsoonData objects from a text file generated by - MonsoonData.save_to_text_file. - - Args: - file_path: The full path of the file load from, including the file - name. - - Returns: - A list of MonsoonData objects. - """ - results = [] - with open(file_path, 'r') as f: - data_strs = f.read().split(MonsoonData.delimiter) - data_strs = data_strs[:-1] - for data_str in data_strs: - results.append(MonsoonData.from_string(data_str)) - return results - - def _validate_data(self): - """Verifies that the data points contained in the class are valid. - """ - msg = "Error! Expected {} timestamps, found {}.".format( - len(self._data_points), len(self._timestamps)) - if len(self._data_points) != len(self._timestamps): - raise MonsoonError(msg) - - def update_offset(self, new_offset): - """Updates how many data points to skip in caculations. - - Always use this function to update offset instead of directly setting - self.offset. - - Args: - new_offset: The new offset. - """ - self.offset = new_offset - self.data_points = self._data_points[self.offset:] - self.timestamps = self._timestamps[self.offset:] - - def get_data_with_timestamps(self): - """Returns the data points with timestamps. - - Returns: - A list of tuples in the format of (timestamp, data) - """ - result = [] - for t, d in zip(self.timestamps, self.data_points): - result.append(t, round(d, self.lr)) - return result - - def get_average_record(self, n): - """Returns a list of average current numbers, each representing the - average over the last n data points. - - Args: - n: Number of data points to average over. - - Returns: - A list of average current values. - """ - history_deque = collections.deque() - averages = [] - for d in self.data_points: - history_deque.appendleft(d) - if len(history_deque) > n: - history_deque.pop() - avg = sum(history_deque) / len(history_deque) - averages.append(round(avg, self.lr)) - return averages - - def _header(self): - strs = [""] - if self.tag: - strs.append(self.tag) + for serial in configs: + serial_number = int(serial) + if serial_number < 20000: + # This code assumes the LVPM has not been updated to have a + # non-stock firmware. If someone has updated the firmware, + # power measurement will fail. + objs.append(LvpmStockMonsoon(serial=serial_number)) else: - strs.append("Monsoon Measurement Data") - strs.append("Average Current: {}mA.".format(self.average_current)) - strs.append("Voltage: {}V.".format(self.voltage)) - strs.append("Total Power: {}mW.".format(self.total_power)) - strs.append( - ("{} samples taken at {}Hz, with an offset of {} samples.").format( - len(self._data_points), self.hz, self.offset)) - return "\n".join(strs) - - def __len__(self): - return len(self.data_points) - - def __str__(self): - strs = [] - strs.append(self._header()) - strs.append("Time" + ' ' * 7 + "Amp") - for t, d in zip(self.timestamps, self.data_points): - strs.append("{} {}".format(t, round(d, self.sr))) - return "\n".join(strs) - - def __repr__(self): - return self._header() - - -class Monsoon(object): - """The wrapper class for test scripts to interact with monsoon. - """ - - def __init__(self, *args, **kwargs): - serial = kwargs["serial"] - device = None - self.log = logging.getLogger() - if "device" in kwargs: - device = kwargs["device"] - self.mon = MonsoonProxy(serialno=serial, device=device) - self.dev = self.mon.ser.name - self.serial = serial - self.dut = None - - def attach_device(self, dut): - """Attach the controller object for the Device Under Test (DUT) - physically attached to the Monsoon box. - - Args: - dut: A controller object representing the device being powered by - this Monsoon box. - """ - self.dut = dut - - def set_voltage(self, volt, ramp=False): - """Sets the output voltage of monsoon. - - Args: - volt: Voltage to set the output to. - ramp: If true, the output voltage will be increased gradually to - prevent tripping Monsoon overvoltage. - """ - if ramp: - self.mon.RampVoltage(mon.start_voltage, volt) - else: - self.mon.SetVoltage(volt) - - def set_max_current(self, cur): - """Sets monsoon's max output current. - - Args: - cur: The max current in A. - """ - self.mon.SetMaxCurrent(cur) - - def set_max_init_current(self, cur): - """Sets the max power-up/inital current. - - Args: - cur: The max initial current allowed in mA. - """ - self.mon.SetMaxPowerUpCurrent(cur) - - @property - def status(self): - """Gets the status params of monsoon. - - Returns: - A dictionary where each key-value pair represents a monsoon status - param. - """ - return self.mon.GetStatus() - - def take_samples(self, sample_hz, sample_num, sample_offset=0, live=False): - """Take samples of the current value supplied by monsoon. - - This is the actual measurement for power consumption. This function - blocks until the number of samples requested has been fulfilled. - - Args: - hz: Number of points to take for every second. - sample_num: Number of samples to take. - offset: The number of initial data points to discard in MonsoonData - calculations. sample_num is extended by offset to compensate. - live: Print each sample in console as measurement goes on. - - Returns: - A MonsoonData object representing the data obtained in this - sampling. None if sampling is unsuccessful. - """ - sys.stdout.flush() - voltage = self.mon.GetVoltage() - self.log.info("Taking samples at %dhz for %ds, voltage %.2fv.", - sample_hz, (sample_num / sample_hz), voltage) - sample_num += sample_offset - # Make sure state is normal - self.mon.StopDataCollection() - status = self.mon.GetStatus() - native_hz = status["sampleRate"] * 1000 - - # Collect and average samples as specified - self.mon.StartDataCollection() - - # In case sample_hz doesn't divide native_hz exactly, use this - # invariant: 'offset' = (consumed samples) * sample_hz - - # (emitted samples) * native_hz - # This is the error accumulator in a variation of Bresenham's - # algorithm. - emitted = offset = 0 - collected = [] - # past n samples for rolling average - history_deque = collections.deque() - current_values = [] - timestamps = [] - - try: - last_flush = time.time() - while emitted < sample_num or sample_num == -1: - # The number of raw samples to consume before emitting the next - # output - need = int((native_hz - offset + sample_hz - 1) / sample_hz) - if need > len(collected): # still need more input samples - samples = self.mon.CollectData() - if not samples: - break - collected.extend(samples) - else: - # Have enough data, generate output samples. - # Adjust for consuming 'need' input samples. - offset += need * sample_hz - # maybe multiple, if sample_hz > native_hz - while offset >= native_hz: - # TODO(angli): Optimize "collected" operations. - this_sample = sum(collected[:need]) / need - this_time = int(time.time()) - timestamps.append(this_time) - if live: - self.log.info("%s %s", this_time, this_sample) - current_values.append(this_sample) - sys.stdout.flush() - offset -= native_hz - emitted += 1 # adjust for emitting 1 output sample - collected = collected[need:] - now = time.time() - if now - last_flush >= 0.99: # flush every second - sys.stdout.flush() - last_flush = now - except Exception as e: - pass - self.mon.StopDataCollection() - try: - return MonsoonData( - current_values, - timestamps, - sample_hz, - voltage, - offset=sample_offset) - except: - return None - - @utils.timeout(60) - def usb(self, state): - """Sets the monsoon's USB passthrough mode. This is specific to the - USB port in front of the monsoon box which connects to the powered - device, NOT the USB that is used to talk to the monsoon itself. - - "Off" means USB always off. - "On" means USB always on. - "Auto" means USB is automatically turned off when sampling is going on, - and turned back on when sampling finishes. - - Args: - stats: The state to set the USB passthrough to. - - Returns: - True if the state is legal and set. False otherwise. - """ - state_lookup = {"off": 0, "on": 1, "auto": 2} - state = state.lower() - if state in state_lookup: - current_state = self.mon.GetUsbPassthrough() - while (current_state != state_lookup[state]): - self.mon.SetUsbPassthrough(state_lookup[state]) - time.sleep(1) - current_state = self.mon.GetUsbPassthrough() - return True - return False - - def _check_dut(self): - """Verifies there is a DUT attached to the monsoon. - - This should be called in the functions that operate the DUT. - """ - if not self.dut: - raise MonsoonError("Need to attach the device before using it.") - - @utils.timeout(15) - def _wait_for_device(self, ad): - while ad.serial not in android_device.list_adb_devices(): - pass - ad.adb.wait_for_device() - - def execute_sequence_and_measure(self, - step_funcs, - hz, - duration, - offset_sec=20, - *args, - **kwargs): - """@Deprecated. - Executes a sequence of steps and take samples in-between. - - For each step function, the following steps are followed: - 1. The function is executed to put the android device in a state. - 2. If the function returns False, skip to next step function. - 3. If the function returns True, sl4a session is disconnected. - 4. Monsoon takes samples. - 5. Sl4a is reconnected. - - Because it takes some time for the device to calm down after the usb - connection is cut, an offset is set for each measurement. The default - is 20s. - - Args: - hz: Number of samples to take per second. - durations: Number(s) of minutes to take samples for in each step. - If this is an integer, all the steps will sample for the same - amount of time. If this is an iterable of the same length as - step_funcs, then each number represents the number of minutes - to take samples for after each step function. - e.g. If durations[0] is 10, we'll sample for 10 minutes after - step_funcs[0] is executed. - step_funcs: A list of funtions, whose first param is an android - device object. If a step function returns True, samples are - taken after this step, otherwise we move on to the next step - function. - ad: The android device object connected to this monsoon. - offset_sec: The number of seconds of initial data to discard. - *args, **kwargs: Extra args to be passed into each step functions. - - Returns: - The MonsoonData objects from samplings. - """ - self._check_dut() - sample_nums = [] - try: - if len(duration) != len(step_funcs): - raise MonsoonError(("The number of durations need to be the " - "same as the number of step functions.")) - for d in duration: - sample_nums.append(d * 60 * hz) - except TypeError: - num = duration * 60 * hz - sample_nums = [num] * len(step_funcs) - results = [] - oset = offset_sec * hz - for func, num in zip(step_funcs, sample_nums): - try: - self.usb("auto") - step_name = func.__name__ - self.log.info("Executing step function %s.", step_name) - take_sample = func(ad, *args, **kwargs) - if not take_sample: - self.log.info("Skip taking samples for %s", step_name) - continue - time.sleep(1) - self.dut.stop_services() - time.sleep(1) - self.log.info("Taking samples for %s.", step_name) - data = self.take_samples(hz, num, sample_offset=oset) - if not data: - raise MonsoonError("Sampling for %s failed." % step_name) - self.log.info("Sample summary: %s", repr(data)) - data.tag = step_name - results.append(data) - except Exception: - self.log.exception("Exception happened during step %s, abort!" - % func.__name__) - return results - finally: - self.mon.StopDataCollection() - self.usb("on") - self._wait_for_device(self.dut) - # Wait for device to come back online. - time.sleep(10) - self.dut.start_services() - # Release wake lock to put device into sleep. - self.dut.droid.goToSleepNow() - return results - - def disconnect_dut(self): - """Disconnect DUT from monsoon. - - Stop the sl4a service on the DUT and disconnect USB connection - raises: - MonsoonError: monsoon erro trying to disconnect usb - """ - try: - self.dut.stop_services() - time.sleep(1) - self.usb("off") - except Exception as e: - raise MonsoonError( - "Error happended trying to disconnect DUT from Monsoon") - - def monsoon_usb_auto(self): - """Set monsoon USB to auto to ready the device for power measurement. - - Stop the sl4a service on the DUT and disconnect USB connection - raises: - MonsoonError: monsoon erro trying to set usbpassthrough to auto - """ - try: - self.dut.stop_services() - time.sleep(1) - self.usb("auto") - except Exception as e: - raise MonsoonError( - "Error happended trying to set Monsoon usbpassthrough to auto") - - def reconnect_dut(self): - """Reconnect DUT to monsoon and start sl4a services. - - raises: - MonsoonError: monsoon erro trying to reconnect usb - Turn usbpassthrough on and start the sl4a services. - """ - self.log.info("Reconnecting dut.") - try: - # If wait for device failed, reset monsoon and try it again, if - # this still fails, then raise - try: - self._wait_for_device(self.dut) - except acts.utils.TimeoutError: - self.log.info('Retry-reset monsoon and connect again') - self.usb('off') - time.sleep(1) - self.usb('on') - self._wait_for_device(self.dut) - # Wait for device to come back online. - time.sleep(2) - self.dut.start_services() - # Release wake lock to put device into sleep. - self.dut.droid.goToSleepNow() - self.log.info("Dut reconnected.") - except Exception as e: - raise MonsoonError("Error happened trying to reconnect DUT") - - def measure_power(self, hz, duration, tag, offset=30): - """Measure power consumption of the attached device. - - Because it takes some time for the device to calm down after the usb - connection is cut, an offset is set for each measurement. The default - is 30s. The total time taken to measure will be (duration + offset). - - Args: - hz: Number of samples to take per second. - duration: Number of seconds to take samples for in each step. - offset: The number of seconds of initial data to discard. - tag: A string that's the name of the collected data group. - - Returns: - A MonsoonData object with the measured power data. - """ - num = duration * hz - oset = offset * hz - data = None - try: - data = self.take_samples(hz, num, sample_offset=oset) - if not data: - raise MonsoonError( - ("No data was collected in measurement %s.") % tag) - data.tag = tag - self.log.info("Measurement summary: %s", repr(data)) - finally: - self.mon.StopDataCollection() - return data + objs.append(HvpmMonsoon(serial=serial_number)) + return objs - def reconnect_monsoon(self): - """Reconnect Monsoon to serial port. - """ - logging.info("Close serial connection") - self.mon.ser.close() - logging.info("Reset serial port") - time.sleep(5) - logging.info("Open serial connection") - self.mon.ser.open() - self.mon.ser.reset_input_buffer() - self.mon.ser.reset_output_buffer() +def destroy(monsoons): + for monsoon in monsoons: + monsoon.release_monsoon_connection() diff --git a/acts/framework/acts/controllers/monsoon_lib/api/common.py b/acts/framework/acts/controllers/monsoon_lib/api/common.py index fffed4cbd3..f932535467 100644 --- a/acts/framework/acts/controllers/monsoon_lib/api/common.py +++ b/acts/framework/acts/controllers/monsoon_lib/api/common.py @@ -40,7 +40,37 @@ PASSTHROUGH_STATES = { } -class MonsoonData(object): +class MonsoonDataRecord(object): + """A data class for Monsoon data points.""" + def __init__(self, time, current): + """Creates a new MonsoonDataRecord. + + Args: + time: the string '{time}s', where time is measured in seconds since + the beginning of the data collection. + current: The current in Amperes as a string. + """ + self._time = float(time[:-1]) + self._current = float(current) + + @property + def time(self): + """The time the record was fetched.""" + return self._time + + @property + def current(self): + """The amount of current in Amperes measured for the given record.""" + return self._current + + @classmethod + def create_from_record_line(cls, line): + """Creates a data record from the line passed in from the output file. + """ + return cls(*line.split(' ')) + + +class MonsoonResult(object): """An object that contains aggregated data collected during sampling. Attributes: @@ -53,19 +83,48 @@ class MonsoonData(object): # The number of decimal places to round a value to. ROUND_TO = 6 - def __init__(self, num_samples, sum_currents, hz, voltage, tag=None): + def __init__(self, num_samples, sum_currents, hz, voltage, datafile_path): + """Creates a new MonsoonResult. + + Args: + num_samples: the number of samples collected. + sum_currents: the total summation of every current measurement. + hz: the number of samples per second. + voltage: the voltage used during the test. + datafile_path: the path to the monsoon data file. + """ self._num_samples = num_samples self._sum_currents = sum_currents self._hz = hz self._voltage = voltage - self.tag = tag + self.tag = datafile_path + + def get_data_points(self): + """Returns an iterator of MonsoonDataRecords.""" + class MonsoonDataIterator: + def __init__(self, file): + self.file = file + + def __iter__(self): + with open(self.file, 'r') as f: + for line in f: + # Remove the newline character. + line.strip() + yield MonsoonDataRecord.create_from_record_line(line) + + return MonsoonDataIterator(self.tag) + + @property + def num_samples(self): + """The number of samples recorded during the test.""" + return self._num_samples @property def average_current(self): """Average current in mA.""" - if self._num_samples == 0: + if self.num_samples == 0: return 0 - return round(self._sum_currents * 1000 / self._num_samples, + return round(self._sum_currents * 1000 / self.num_samples, self.ROUND_TO) @property @@ -79,8 +138,14 @@ class MonsoonData(object): """Total power used.""" return round(self.average_current * self._voltage, self.ROUND_TO) + @property + def voltage(self): + """The voltage during the measurement (in Volts).""" + return self._voltage + def __str__(self): return ('avg current: %s\n' 'total charge: %s\n' - 'total power: %s' % (self.average_current, self.total_charge, - self.total_power)) + 'total power: %s\n' + 'total samples: %s' % (self.average_current, self.total_charge, + self.total_power, self._num_samples)) diff --git a/acts/framework/acts/controllers/monsoon_lib/api/hvpm/monsoon.py b/acts/framework/acts/controllers/monsoon_lib/api/hvpm/monsoon.py index 6dc72c8f91..f1b03c9114 100644 --- a/acts/framework/acts/controllers/monsoon_lib/api/hvpm/monsoon.py +++ b/acts/framework/acts/controllers/monsoon_lib/api/hvpm/monsoon.py @@ -20,7 +20,7 @@ import time from Monsoon import HVPM from Monsoon import Operations as op -from acts.controllers.monsoon_lib.api.common import MonsoonData +from acts.controllers.monsoon_lib.api.common import MonsoonResult from acts.controllers.monsoon_lib.api.monsoon import BaseMonsoon from acts.controllers.monsoon_lib.sampling.engine.assembly_line import AssemblyLineBuilder from acts.controllers.monsoon_lib.sampling.engine.assembly_line import ThreadAssemblyLine @@ -137,8 +137,9 @@ class Monsoon(BaseMonsoon): manager.shutdown() self._mon.setup_usb(self.serial) - monsoon_data = MonsoonData(aggregator.num_samples, - aggregator.sum_currents, hz, voltage) + monsoon_data = MonsoonResult(aggregator.num_samples, + aggregator.sum_currents, hz, voltage, + output_path) self._log.info('Measurement summary:\n%s', str(monsoon_data)) return monsoon_data diff --git a/acts/framework/acts/controllers/monsoon_lib/api/lvpm_stock/monsoon.py b/acts/framework/acts/controllers/monsoon_lib/api/lvpm_stock/monsoon.py index b54b6794fc..e8d116d83f 100644 --- a/acts/framework/acts/controllers/monsoon_lib/api/lvpm_stock/monsoon.py +++ b/acts/framework/acts/controllers/monsoon_lib/api/lvpm_stock/monsoon.py @@ -16,7 +16,7 @@ import multiprocessing import time -from acts.controllers.monsoon_lib.api.common import MonsoonData +from acts.controllers.monsoon_lib.api.common import MonsoonResult from acts.controllers.monsoon_lib.api.lvpm_stock.monsoon_proxy import MonsoonProxy from acts.controllers.monsoon_lib.api.monsoon import BaseMonsoon from acts.controllers.monsoon_lib.sampling.engine.assembly_line import AssemblyLineBuilder @@ -119,8 +119,9 @@ class Monsoon(BaseMonsoon): manager.shutdown() - monsoon_data = MonsoonData(aggregator.num_samples, - aggregator.sum_currents, hz, voltage) + monsoon_data = MonsoonResult(aggregator.num_samples, + aggregator.sum_currents, hz, voltage, + output_path) self._log.info('Measurement summary:\n%s', str(monsoon_data)) return monsoon_data diff --git a/acts/framework/acts/controllers/monsoon_lib/api/monsoon.py b/acts/framework/acts/controllers/monsoon_lib/api/monsoon.py index 4916da2f15..a55fc8a0e0 100644 --- a/acts/framework/acts/controllers/monsoon_lib/api/monsoon.py +++ b/acts/framework/acts/controllers/monsoon_lib/api/monsoon.py @@ -160,6 +160,8 @@ class BaseMonsoon(object): """Deprecated. Use the connection callbacks instead.""" def on_reconnect(): + # Make sure the device is connected and available for commands. + android_device.wait_for_boot_completion() android_device.start_services() # Release wake lock to put device into sleep. android_device.droid.goToSleepNow() @@ -180,15 +182,6 @@ class BaseMonsoon(object): """Sets the callback to be called when Monsoon reconnects USB.""" self.on_reconnect = callback - def monsoon_usb_auto(self): - """Deprecated. Use usb('auto') or usb(PassthroughStates.AUTO) instead. - """ - self.usb(PassthroughStates.AUTO) - - def reconnect_dut(self): - """Deprecated. Use usb('on') or usb(PassthroughStates.ON) instead.""" - self.usb(PassthroughStates.ON) - def take_samples(self, assembly_line): """Runs the sampling procedure based on the given assembly line.""" # Sampling is always done in a separate process. Release the Monsoon diff --git a/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500.py b/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500.py index e717e8c5e5..0163d16517 100644 --- a/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500.py +++ b/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500.py @@ -73,7 +73,6 @@ class TransmissionModes(Enum): TM2 = 'TM2' TM3 = 'TM3' TM4 = 'TM4' - TM6 = 'TM6' TM7 = 'TM7' TM8 = 'TM8' TM9 = 'TM9' @@ -122,6 +121,13 @@ class MimoModes(Enum): MIMO4x4 = 'FOUR' +class MimoScenario(Enum): + """Supportted mimo scenarios""" + SCEN1x1 = 'SCELl:FLEXible SUA1,RF1C,RX1,RF1C,TX1' + SCEN2x2 = 'TRO:FLEXible SUA1,RF1C,RX1,RF1C,TX1,RF3C,TX2' + SCEN4x4 = 'FRO FLEXible SUA1,RF1C,RX1,RF1C,TX1,RF3C,TX2,RF2C,TX3,RF4C,TX4' + + class RrcState(Enum): """States to enable/disable rrc.""" RRC_ON = 'ON' @@ -195,11 +201,14 @@ class Cmw500(abstract_inst.SocketInstrument): status = self._recv() return status - def set_mimo(self): - """Sets the scenario for the test.""" - # TODO:(ganeshganesh) Create a common function to set mimo modes. - self.send_and_recv('ROUTe:LTE:SIGN:SCENario:SCELl:FLEXible SUW1,RF1C,' - 'RX1,RF1C,TX1') + def configure_mimo_settings(self, mimo): + """Sets the mimo scenario for the test. + + Args: + mimo: mimo scenario to set. + """ + cmd = 'ROUTe:LTE:SIGN:SCENario:{}'.format(mimo.value) + self.send_and_recv(cmd) def wait_for_lte_state_change(self, timeout=20): """Waits until the state of LTE changes. @@ -244,7 +253,7 @@ class Cmw500(abstract_inst.SocketInstrument): else: raise CmwError('Failure in setting up/detaching connection') - def wait_for_connected_state(self, timeout=120): + def wait_for_attached_state(self, timeout=120): """Attach the controller with device. Args: @@ -263,10 +272,22 @@ class Cmw500(abstract_inst.SocketInstrument): else: raise CmwError('Device could not be attached') - conn_state = self.send_and_recv('SENSe:LTE:SIGN:RRCState?') + def wait_for_connected_state(self, timeout=120): + """Checks if controller connected with device. + + Args: + timeout: timeout for phone to be in connnected state. - if conn_state == LTE_CONN_RESP: - self._logger.debug('Call box connected with device') + Raises: + CmwError on time out. + """ + end_time = time.time() + timeout + while time.time() <= end_time: + conn_state = self.send_and_recv('SENSe:LTE:SIGN:RRCState?') + + if conn_state == LTE_CONN_RESP: + self._logger.debug('Call box connected with device') + break else: raise CmwError('Call box could not be connected with device') @@ -280,7 +301,7 @@ class Cmw500(abstract_inst.SocketInstrument): return self.send_and_recv('*IDN?') def disconnect(self): - """Detach controller from device and switch to local mode.""" + """Disconnect controller from device and switch to local mode.""" self.switch_lte_signalling(LteState.LTE_OFF) self.close_remote_mode() self._close_socket() @@ -289,6 +310,10 @@ class Cmw500(abstract_inst.SocketInstrument): """Exits remote mode to local mode.""" self.send_and_recv('>L') + def detach(self): + """Detach callbox and controller.""" + self.send_and_recv('CALL:LTE:SIGN:PSWitched:ACTion DETach') + @property def rrc_connection(self): """Gets the RRC connection state.""" @@ -570,19 +595,18 @@ class BaseStation(object): self._bts, mode.value) self._cmw.send_and_recv(cmd) - # TODO: (@sairamganesh) find a common way to set parameters for rmc and - # user defined channels(udch) scheduling. @property - def rmc_rb_configuration_dl(self): + def rb_configuration_dl(self): """Gets rmc's rb configuration for down link. This function returns Number of Resource blocks, Resource block position and Modulation type. """ - cmd = 'CONFigure:LTE:SIGN:CONNection:{}:RMC:DL?'.format(self._bts) + cmd = 'CONFigure:LTE:SIGN:CONNection:{}:{}:DL?'.format( + self._bts, self.scheduling_mode) return self._cmw.send_and_recv(cmd) - @rmc_rb_configuration_dl.setter - def rmc_rb_configuration_dl(self, rb_config): - """Sets the rb configuration for down link with scheduling type RMC. + @rb_configuration_dl.setter + def rb_configuration_dl(self, rb_config): + """Sets the rb configuration for down link for scheduling type. Args: rb_config: Tuple containing Number of resource blocks, resource @@ -591,100 +615,59 @@ class BaseStation(object): Raises: ValueError: If tuple unpacking fails. """ - rb, rb_pos, modulation = rb_config + if self.scheduling_mode == 'RMC': + rb, rb_pos, modulation = rb_config - cmd = ('CONFigure:LTE:SIGN:CONNection:{}:RMC:DL {},{},' - '{}'.format(self._bts, rb, rb_pos, modulation)) - self._cmw.send_and_recv(cmd) + cmd = ('CONFigure:LTE:SIGN:CONNection:{}:RMC:DL {},{},' + '{}'.format(self._bts, rb, rb_pos, modulation)) + self._cmw.send_and_recv(cmd) - @property - def rmc_rb_configuration_ul(self): - """Gets rmc's rb configuration for up link. This function returns - Number of Resource blocks, Resource block position and Modulation type. - """ - cmd = 'CONFigure:LTE:SIGN:CONNection:{}:RMC:UL?'.format(self._bts) - return self._cmw.send_and_recv(cmd) + elif self.scheduling_mode == 'UDCH': + rb, start_rb, modulation, tbs = rb_config - @rmc_rb_configuration_ul.setter - def rmc_rb_configuration_ul(self, rb_config): - """Sets the rb configuration for up link with scheduling type RMC. + if not 0 <= rb <= 26: + raise ValueError('rb should be between 0 and 26 inclusive.') - Args: - rb_config: Tuple containing Number of resource blocks, resource - block position and modulation type. - - Raises: - ValueError: If tuple unpacking fails. - """ - rb, rb_pos, modulation = rb_config - - cmd = ('CONFigure:LTE:SIGN:CONNection:{}:RMC:UL {},{},' - '{}'.format(self._bts, rb, rb_pos, modulation)) - self._cmw.send_and_recv(cmd) + cmd = ('CONFigure:LTE:SIGN:CONNection:{}:UDCHannels:DL {},{},' + '{},{}'.format(self._bts, rb, start_rb, modulation, tbs)) + self._cmw.send_and_recv(cmd) @property - def udch_rb_configuration_dl(self): - """Gets udch's rb configuration for down link. This function returns - Number of Resource blocks, Resource block position, Modulation type and - tbs. + def rb_configuration_ul(self): + """Gets rb configuration for up link. This function returns + Number of Resource blocks, Resource block position and Modulation type. """ - cmd = 'CONFigure:LTE:SIGN:CONNection:{}:UDCH:DL?'.format(self._bts) + cmd = 'CONFigure:LTE:SIGN:CONNection:{}:{}:UL?'.format( + self._bts, self.scheduling_mode) return self._cmw.send_and_recv(cmd) - @udch_rb_configuration_dl.setter - def udch_rb_configuration_dl(self, rb_config): - """Sets the rb configuration for down link with scheduling type RMC. + @rb_configuration_ul.setter + def rb_configuration_ul(self, rb_config): + """Sets the rb configuration for down link for scheduling mode. Args: rb_config: Tuple containing Number of resource blocks, resource - block position, modulation type and tbs value. + block position and modulation type. Raises: ValueError: If tuple unpacking fails. """ - rb, start_rb, md_type, tbs = rb_config - - if rb not in range(0, 51): - raise ValueError('rb should be between 0 and 50 inclusive.') - - if not isinstance(md_type, ModulationType): - raise ValueError('md_type should be the instance of ModulationType.') - - cmd = ('CONFigure:LTE:SIGN:CONNection:{}:udch:DL {},{},' - '{},{}'.format(self._bts, rb, start_rb, md_type, tbs)) - self._cmw.send_and_recv(cmd) - - @property - def udch_rb_configuration_ul(self): - """Gets udch's rb configuration for up link. This function returns - Number of Resource blocks, Resource block position, Modulation type - and tbs. - """ - cmd = 'CONFigure:LTE:SIGN:CONNection:{}:UDCH:UL?'.format(self._bts) - return self._cmw.send_and_recv(cmd) - - @udch_rb_configuration_ul.setter - def udch_rb_configuration_ul(self, rb_config): - """Sets the rb configuration for up link with scheduling type RMC. + if self.scheduling_mode == 'RMC': + rb, rb_pos, modulation = rb_config - Args: - rb_config: Tuple containing Number of resource blocks, resource - block position, modulation type and tbs value. - - Raises: - ValueError: If tuple unpacking fails/Specified out of range. - """ - rb, start_rb, md_type, tbs = rb_config + cmd = ('CONFigure:LTE:SIGN:CONNection:{}:RMC:UL {},{},' + '{}'.format(self._bts, rb, rb_pos, modulation)) + self._cmw.send_and_recv(cmd) - if rb not in range(0, 51): - raise ValueError('rb should be between 0 and 50 inclusive.') + elif self.scheduling_mode == 'UDCH': + rb, start_rb, modulation, tbs = rb_config - if not isinstance(md_type, ModulationType): - raise ValueError('md_type should be the instance of ModulationType.') + if not 0 <= rb <= 26: + raise ValueError('rb should be between 0 and 26 inclusive.') - cmd = ('CONFigure:LTE:SIGN:CONNection:{}:udch:UL {},{},' - '{},{}'.format(self._bts, rb, start_rb, md_type, tbs)) - self._cmw.send_and_recv(cmd) + cmd = ('CONFigure:LTE:SIGN:CONNection:{}:UDCHannels:UL {},{},' + '{},{}'.format(self._bts, rb, start_rb, modulation, tbs)) + self._cmw.send_and_recv(cmd) @property def rb_position_dl(self): @@ -767,6 +750,8 @@ class BaseStation(object): Args: num_antenna: Count of number of dl antennas to use. """ + if not isinstance(num_antenna, MimoModes): + raise ValueError('num_antenna should be an instance of MimoModes.') cmd = 'CONFigure:LTE:SIGN:CONNection:{}:NENBantennas {}'.format( self._bts, num_antenna) self._cmw.send_and_recv(cmd) diff --git a/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500_cellular_simulator.py b/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500_cellular_simulator.py index 6c76e62f7a..64d127b808 100644 --- a/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500_cellular_simulator.py +++ b/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500_cellular_simulator.py @@ -13,11 +13,34 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import time + from acts.controllers.rohdeschwarz_lib import cmw500 from acts.controllers import cellular_simulator as cc - - -class CMW500CellularSimulator: +from acts.test_utils.power.tel_simulations import LteSimulation + +CMW_TM_MAPPING = { + LteSimulation.TransmissionMode.TM1: cmw500.TransmissionModes.TM1, + LteSimulation.TransmissionMode.TM2: cmw500.TransmissionModes.TM2, + LteSimulation.TransmissionMode.TM3: cmw500.TransmissionModes.TM3, + LteSimulation.TransmissionMode.TM4: cmw500.TransmissionModes.TM4, + LteSimulation.TransmissionMode.TM7: cmw500.TransmissionModes.TM7, + LteSimulation.TransmissionMode.TM8: cmw500.TransmissionModes.TM8, + LteSimulation.TransmissionMode.TM9: cmw500.TransmissionModes.TM9 +} + +CMW_SCH_MAPPING = { + LteSimulation.SchedulingMode.STATIC: cmw500.SchedulingMode.USERDEFINEDCH +} + +CMW_MIMO_MAPPING = { + LteSimulation.MimoMode.MIMO_1x1: cmw500.MimoModes.MIMO1x1, + LteSimulation.MimoMode.MIMO_2x2: cmw500.MimoModes.MIMO2x2, + LteSimulation.MimoMode.MIMO_4x4: cmw500.MimoModes.MIMO4x4 +} + + +class CMW500CellularSimulator(cc.AbstractCellularSimulator): """ A cellular simulator for telephony simulations based on the CMW 500 controller. """ @@ -40,24 +63,43 @@ class CMW500CellularSimulator: ip_address: the ip address of the CMW500 port: the port number for the CMW500 controller """ + super().__init__() + try: self.cmw = cmw500.Cmw500(ip_address, port) except cmw500.CmwError: raise cc.CellularSimulatorError('Could not connect to CMW500.') + self.bts = None + self.dl_modulation = None + self.ul_modulation = None + def destroy(self): """ Sends finalization commands to the cellular equipment and closes the connection. """ - raise NotImplementedError() + self.cmw.disconnect() def setup_lte_scenario(self): """ Configures the equipment for an LTE simulation. """ - raise NotImplementedError() + self.bts = [self.cmw.get_base_station()] + self.cmw.switch_lte_signalling(cmw500.LteState.LTE_ON) def setup_lte_ca_scenario(self): """ Configures the equipment for an LTE with CA simulation. """ raise NotImplementedError() + def set_lte_rrc_state_change_timer(self, enabled, time=10): + """ Configures the LTE RRC state change timer. + + Args: + enabled: a boolean indicating if the timer should be on or off. + time: time in seconds for the timer to expire + """ + # Setting this method to pass instead of raising an exception as it + # it is required by LTE sims. + # TODO (b/141838145): Implement RRC status change timer for CMW500. + pass + def set_band(self, bts_index, band): """ Sets the band for the indicated base station. @@ -65,7 +107,25 @@ class CMW500CellularSimulator: bts_index: the base station number band: the new band """ - raise NotImplementedError() + bts = self.bts[bts_index] + bts.duplex_mode = self.get_duplex_mode(band) + band = 'OB' + band + bts.band = band + self.log.debug('Band set to {}'.format(band)) + + def get_duplex_mode(self, band): + """ Determines if the band uses FDD or TDD duplex mode + + Args: + band: a band number + + Returns: + an variable of class DuplexMode indicating if band is FDD or TDD + """ + if 33 <= int(band) <= 46: + return cmw500.DuplexMode.TDD + else: + return cmw500.DuplexMode.FDD def set_input_power(self, bts_index, input_power): """ Sets the input power for the indicated base station. @@ -83,7 +143,8 @@ class CMW500CellularSimulator: bts_index: the base station number output_power: the new output power """ - raise NotImplementedError() + bts = self.bts[bts_index] + bts.downlink_power_level = output_power def set_tdd_config(self, bts_index, tdd_config): """ Sets the tdd configuration number for the indicated base station. @@ -92,7 +153,7 @@ class CMW500CellularSimulator: bts_index: the base station number tdd_config: the new tdd configuration number """ - raise NotImplementedError() + self.bts[bts_index].uldl_configuration = tdd_config def set_bandwidth(self, bts_index, bandwidth): """ Sets the bandwidth for the indicated base station. @@ -101,7 +162,23 @@ class CMW500CellularSimulator: bts_index: the base station number bandwidth: the new bandwidth """ - raise NotImplementedError() + bts = self.bts[bts_index] + + if bandwidth == 20: + bts.bandwidth = cmw500.LteBandwidth.BANDWIDTH_20MHz + elif bandwidth == 15: + bts.bandwidth = cmw500.LteBandwidth.BANDWIDTH_15MHz + elif bandwidth == 10: + bts.bandwidth = cmw500.LteBandwidth.BANDWIDTH_10MHz + elif bandwidth == 5: + bts.bandwidth = cmw500.LteBandwidth.BANDWIDTH_5MHz + elif bandwidth == 3: + bts.bandwidth = cmw500.LteBandwidth.BANDWIDTH_3MHz + elif bandwidth == 1.4: + bts.bandwidth = cmw500.LteBandwidth.BANDWIDTH_1MHz + else: + msg = 'Bandwidth {} MHz is not valid for LTE'.format(bandwidth) + raise ValueError(msg) def set_downlink_channel_number(self, bts_index, channel_number): """ Sets the downlink channel number for the indicated base station. @@ -110,7 +187,9 @@ class CMW500CellularSimulator: bts_index: the base station number channel_number: the new channel number """ - raise NotImplementedError() + bts = self.bts[bts_index] + bts.dl_channel = channel_number + self.log.debug('Downlink Channel set to {}'.format(bts.dl_channel)) def set_mimo_mode(self, bts_index, mimo_mode): """ Sets the mimo mode for the indicated base station. @@ -119,30 +198,103 @@ class CMW500CellularSimulator: bts_index: the base station number mimo_mode: the new mimo mode """ - raise NotImplementedError() - - def set_transmission_mode(self, bts_index, transmission_mode): + bts = self.bts[bts_index] + mimo_mode = CMW_MIMO_MAPPING[mimo_mode] + if mimo_mode == cmw500.MimoModes.MIMO1x1: + self.cmw.configure_mimo_settings(cmw500.MimoScenario.SCEN1x1) + bts.dl_antenna = cmw500.MimoModes.MIMO1x1 + + elif mimo_mode == cmw500.MimoModes.MIMO2x2: + self.cmw.configure_mimo_settings(cmw500.MimoScenario.SCEN2x2) + bts.dl_antenna = cmw500.MimoModes.MIMO2x2 + + elif mimo_mode == cmw500.MimoModes.MIMO4x4: + self.cmw.configure_mimo_settings(cmw500.MimoScenario.SCEN4x4) + bts.dl_antenna = cmw500.MimoModes.MIMO4x4 + else: + RuntimeError('The requested MIMO mode is not supported.') + + def set_transmission_mode(self, bts_index, tmode): """ Sets the transmission mode for the indicated base station. Args: bts_index: the base station number - transmission_mode: the new transmission mode + tmode: the new transmission mode """ - raise NotImplementedError() - - def set_scheduling_mode(self, bts_index, scheduling_mode, mcs_dl, mcs_ul, - nrb_dl, nrb_ul): + bts = self.bts[bts_index] + + tmode = CMW_TM_MAPPING[tmode] + if (tmode in [ + cmw500.TransmissionModes.TM1, + cmw500.TransmissionModes.TM7 + ] and bts.dl_antenna != cmw500.MimoModes.MIMO1x1): + bts.transmode = tmode + elif (tmode in cmw500.TransmissionModes.__members__ and + bts.dl_antenna != cmw500.MimoModes.MIMO2x2): + bts.transmode = tmode + elif (tmode in [ + cmw500.TransmissionModes.TM2, + cmw500.TransmissionModes.TM3, + cmw500.TransmissionModes.TM4, + cmw500.TransmissionModes.TM6, + cmw500.TransmissionModes.TM9 + ] and bts.dl_antenna == cmw500.MimoModes.MIMO4x4): + bts.transmode = tmode + + else: + raise ValueError('Transmission modes should support the current ' + 'mimo mode') + + def set_scheduling_mode(self, bts_index, scheduling, mcs_dl=None, + mcs_ul=None, nrb_dl=None, nrb_ul=None): """ Sets the scheduling mode for the indicated base station. Args: - bts_index: the base station number - scheduling_mode: the new scheduling mode - mcs_dl: Downlink MCS (only for STATIC scheduling) - mcs_ul: Uplink MCS (only for STATIC scheduling) - nrb_dl: Number of RBs for downlink (only for STATIC scheduling) - nrb_ul: Number of RBs for uplink (only for STATIC scheduling) + bts_index: the base station number. + scheduling: the new scheduling mode. + mcs_dl: Downlink MCS. + mcs_ul: Uplink MCS. + nrb_dl: Number of RBs for downlink. + nrb_ul: Number of RBs for uplink. """ - raise NotImplementedError() + bts = self.bts[bts_index] + bts.scheduling_mode = CMW_SCH_MAPPING[scheduling] + + if not self.ul_modulation and self.dl_modulation: + raise ValueError('Modulation should be set prior to scheduling ' + 'call') + + if scheduling == cmw500.SchedulingMode.RMC: + + if not nrb_ul and nrb_dl: + raise ValueError('nrb_ul and nrb dl should not be none') + + bts.rb_configuration_ul = (nrb_ul, self.ul_modulation, 'KEEP') + self.log.info('ul rb configurations set to {}'.format( + bts.rb_configuration_ul)) + + time.sleep(1) + + self.log.debug('Setting rb configurations for down link') + bts.rb_configuration_dl = (nrb_dl, self.dl_modulation, 'KEEP') + self.log.info('dl rb configurations set to {}'.format( + bts.rb_configuration_ul)) + + elif scheduling == cmw500.SchedulingMode.USERDEFINEDCH: + + if not all([nrb_ul, nrb_dl, mcs_dl, mcs_ul]): + raise ValueError('All parameters are mandatory.') + + bts.rb_configuration_ul = (nrb_ul, 0, self.ul_modulation, + mcs_ul) + self.log.info('ul rb configurations set to {}'.format( + bts.rb_configuration_ul)) + + time.sleep(1) + + bts.rb_configuration_dl = (nrb_dl, 0, self.dl_modulation, mcs_dl) + self.log.info('dl rb configurations set to {}'.format( + bts.rb_configuration_dl)) def set_enabled_for_ca(self, bts_index, enabled): """ Enables or disables the base station during carrier aggregation. @@ -160,7 +312,12 @@ class CMW500CellularSimulator: bts_index: the base station number modulation: the new DL modulation """ - raise NotImplementedError() + + # This function is only used to store the values of modulation to + # be inline with abstract class signature. + self.dl_modulation = modulation + self.log.warning('Modulation config stored but not applied until ' + 'set_scheduling_mode called.') def set_ul_modulation(self, bts_index, modulation): """ Sets the UL modulation for the indicated base station. @@ -169,7 +326,11 @@ class CMW500CellularSimulator: bts_index: the base station number modulation: the new UL modulation """ - raise NotImplementedError() + # This function is only used to store the values of modulation to + # be inline with abstract class signature. + self.ul_modulation = modulation + self.log.warning('Modulation config stored but not applied until ' + 'set_scheduling_mode called.') def set_tbs_pattern_on(self, bts_index, tbs_pattern_on): """ Enables or disables TBS pattern in the indicated base station. @@ -178,7 +339,8 @@ class CMW500CellularSimulator: bts_index: the base station number tbs_pattern_on: the new TBS pattern setting """ - raise NotImplementedError() + # TODO (b/143918664): CMW500 doesn't have an equivalent setting. + pass def lte_attach_secondary_carriers(self): """ Activates the secondary carriers for CA. Requires the DUT to be @@ -192,7 +354,7 @@ class CMW500CellularSimulator: timeout: after this amount of time the method will raise a CellularSimulatorError exception. Default is 120 seconds. """ - raise NotImplementedError() + self.cmw.wait_for_attached_state(timeout=timeout) def wait_until_communication_state(self, timeout=120): """ Waits until the DUT is in Communication state. @@ -201,7 +363,7 @@ class CMW500CellularSimulator: timeout: after this amount of time the method will raise a CellularSimulatorError exception. Default is 120 seconds. """ - raise NotImplementedError() + self.cmw.wait_for_connected_state(timeout=timeout) def wait_until_idle_state(self, timeout=120): """ Waits until the DUT is in Idle state. @@ -214,7 +376,7 @@ class CMW500CellularSimulator: def detach(self): """ Turns off all the base stations so the DUT loose connection.""" - raise NotImplementedError() + self.cmw.detach() def stop(self): """ Stops current simulation. After calling this method, the simulator diff --git a/acts/framework/acts/test_utils/abstract_devices/bluetooth_handsfree_abstract_device.py b/acts/framework/acts/test_utils/abstract_devices/bluetooth_handsfree_abstract_device.py index bf4d549c5a..bb63bd9934 100644 --- a/acts/framework/acts/test_utils/abstract_devices/bluetooth_handsfree_abstract_device.py +++ b/acts/framework/acts/test_utils/abstract_devices/bluetooth_handsfree_abstract_device.py @@ -14,8 +14,11 @@ # License for the specific language governing permissions and limitations under # the License. import inspect - +import time +from acts import asserts from acts.controllers.buds_lib.dev_utils import apollo_sink_events +from acts.test_utils.bt.bt_constants import bt_default_timeout + def validate_controller(controller, abstract_device_class): @@ -29,8 +32,8 @@ def validate_controller(controller, abstract_device_class): NotImplementedError: if controller is missing one or more methods. """ ctlr_methods = inspect.getmembers(controller, predicate=callable) - reqd_methods = inspect.getmembers(abstract_device_class, - predicate=inspect.ismethod) + reqd_methods = inspect.getmembers( + abstract_device_class, predicate=inspect.ismethod) expected_func_names = {method[0] for method in reqd_methods} controller_func_names = {method[0] for method in ctlr_methods} @@ -47,8 +50,7 @@ def validate_controller(controller, abstract_device_class): if inspect.signature(controller_func) != required_signature: raise NotImplementedError( 'Method {} must have the signature {}{}.'.format( - controller_func.__qualname__, - controller_func.__name__, + controller_func.__qualname__, controller_func.__name__, required_signature)) @@ -58,6 +60,7 @@ class BluetoothHandsfreeAbstractDevice: Desired controller classes should have a corresponding Bluetooth handsfree abstract device class defined in this module. """ + @property def mac_address(self): raise NotImplementedError @@ -115,7 +118,8 @@ class PixelBudsBluetoothHandsfreeAbstractDevice( return self.pixel_buds_controller.bluetooth_address def accept_call(self): - return self.pixel_buds_controller.cmd(self.format_cmd('EventUsrAnswer')) + return self.pixel_buds_controller.cmd( + self.format_cmd('EventUsrAnswer')) def end_call(self): return self.pixel_buds_controller.cmd( @@ -147,7 +151,8 @@ class PixelBudsBluetoothHandsfreeAbstractDevice( self.format_cmd('EventUsrAvrcpSkipBackward')) def reject_call(self): - return self.pixel_buds_controller.cmd(self.format_cmd('EventUsrReject')) + return self.pixel_buds_controller.cmd( + self.format_cmd('EventUsrReject')) def volume_down(self): return self.pixel_buds_controller.volume('Down') @@ -158,7 +163,6 @@ class PixelBudsBluetoothHandsfreeAbstractDevice( class EarstudioReceiverBluetoothHandsfreeAbstractDevice( BluetoothHandsfreeAbstractDevice): - def __init__(self, earstudio_controller): self.earstudio_controller = earstudio_controller @@ -205,7 +209,6 @@ class EarstudioReceiverBluetoothHandsfreeAbstractDevice( class JaybirdX3EarbudsBluetoothHandsfreeAbstractDevice( BluetoothHandsfreeAbstractDevice): - def __init__(self, jaybird_controller): self.jaybird_controller = jaybird_controller @@ -252,13 +255,23 @@ class JaybirdX3EarbudsBluetoothHandsfreeAbstractDevice( class AndroidHeadsetBluetoothHandsfreeAbstractDevice( BluetoothHandsfreeAbstractDevice): - def __init__(self, ad_controller): self.ad_controller = ad_controller @property def mac_address(self): - return self.ad_controller.droid.bluetoothGetLocalAddress() + """Getting device mac with more stability ensurance. + + Sometime, getting mac address is flaky that it returns None. Adding a + loop to add more ensurance of getting correct mac address. + """ + device_mac = None + start_time = time.time() + end_time = start_time + bt_default_timeout + while not device_mac and time.time() < end_time: + device_mac = self.ad_controller.droid.bluetoothGetLocalAddress() + asserts.assert_true(device_mac, 'Can not get the MAC address') + return device_mac def accept_call(self): return self.ad_controller.droid.telecomAcceptRingingCall(None) @@ -271,8 +284,7 @@ class AndroidHeadsetBluetoothHandsfreeAbstractDevice( return self.ad_controller.droid.bluetoothMakeDiscoverable() def next_track(self): - return (self.ad_controller.droid. - bluetoothMediaPassthrough("skipNext")) + return (self.ad_controller.droid.bluetoothMediaPassthrough("skipNext")) def pause(self): return self.ad_controller.droid.bluetoothMediaPassthrough("pause") @@ -287,13 +299,15 @@ class AndroidHeadsetBluetoothHandsfreeAbstractDevice( return self.ad_controller.droid.bluetoothToggleState(True) def previous_track(self): - return (self.ad_controller.droid. - bluetoothMediaPassthrough("skipPrev")) + return (self.ad_controller.droid.bluetoothMediaPassthrough("skipPrev")) def reject_call(self): return self.ad_controller.droid.telecomCallDisconnect( self.ad_controller.droid.telecomCallGetCallIds()[0]) + def reset(self): + return self.ad_controller.droid.bluetoothFactoryReset() + def volume_down(self): target_step = self.ad_controller.droid.getMediaVolume() - 1 target_step = max(target_step, 0) diff --git a/acts/framework/acts/test_utils/bt/BtEnum.py b/acts/framework/acts/test_utils/bt/BtEnum.py index a2010d0074..b9fe6e23e1 100644 --- a/acts/framework/acts/test_utils/bt/BtEnum.py +++ b/acts/framework/acts/test_utils/bt/BtEnum.py @@ -103,3 +103,11 @@ class BluetoothPriorityLevel(Enum): PRIORITY_ON = 100 PRIORITY_OFF = 0 PRIORITY_UNDEFINED = -1 + +class BluetoothA2dpCodecType(Enum): + SBC = 0 + AAC = 1 + APTX = 2 + APTX_HD = 3 + LDAC = 4 + MAX = 5 diff --git a/acts/framework/acts/test_utils/bt/bt_power_test_utils.py b/acts/framework/acts/test_utils/bt/bt_power_test_utils.py index c9c60723e9..628c3375d1 100644 --- a/acts/framework/acts/test_utils/bt/bt_power_test_utils.py +++ b/acts/framework/acts/test_utils/bt/bt_power_test_utils.py @@ -14,93 +14,144 @@ # See the License for the specific language governing permissions and # limitations under the License. +import logging import time +import acts.test_utils.bt.BleEnum as bleenum +import acts.test_utils.instrumentation.instrumentation_command_builder as icb -from acts.test_utils.wifi import wifi_power_test_utils as wputils -from acts.test_utils.bt.bt_test_utils import enable_bluetooth -from acts.test_utils.bt.bt_test_utils import disable_bluetooth - -BT_BASE_UUID = '00000000-0000-1000-8000-00805F9B34FB' -BT_CLASSICAL_DATA = [1, 2, 3] BLE_LOCATION_SCAN_ENABLE = 'settings put global ble_scan_always_enabled 1' BLE_LOCATION_SCAN_DISABLE = 'settings put global ble_scan_always_enabled 0' -START_PMC_CMD = 'am start -n com.android.pmc/com.android.pmc.PMCMainActivity' -PMC_VERBOSE_CMD = 'setprop log.tag.PMC VERBOSE' -PMC_BASE_SCAN = 'am broadcast -a com.android.pmc.BLESCAN --es ScanMode ' +SCREEN_WAIT_TIME = 1 -def phone_setup_for_BT(dut, bt_on, ble_on, screen_status): - """Sets the phone and Bluetooth in the desired state +class MediaControl(object): + """Media control using adb shell for power testing. - Args: - dut: object of the android device under test - bt_on: Enable/Disable BT - ble_on: Enable/Disable BLE - screen_status: screen ON or OFF + Object to control media play status using adb. """ - # Initialize the dut to rock-bottom state - wputils.dut_rockbottom(dut) - time.sleep(2) - - # Check if we are enabling a background scan - # TODO: Turn OFF cellular wihtout having to turn ON airplane mode - if bt_on == 'OFF' and ble_on == 'ON': - dut.adb.shell(BLE_LOCATION_SCAN_ENABLE) - dut.droid.connectivityToggleAirplaneMode(False) - time.sleep(2) - - # Turn ON/OFF BT - if bt_on == 'ON': - enable_bluetooth(dut.droid, dut.ed) - dut.log.info('BT is ON') - else: - disable_bluetooth(dut.droid) - dut.droid.bluetoothDisableBLE() - dut.log.info('BT is OFF') - time.sleep(2) - - # Turn ON/OFF BLE - if ble_on == 'ON': - dut.droid.bluetoothEnableBLE() - dut.log.info('BLE is ON') - else: - dut.droid.bluetoothDisableBLE() - dut.log.info('BLE is OFF') - time.sleep(2) - - # Set the desired screen status - if screen_status == 'OFF': - dut.droid.goToSleepNow() - dut.log.info('Screen is OFF') - time.sleep(2) - - -def start_pmc_ble_scan(dut, - scan_mode, - offset_start, - scan_time, - idle_time=None, - num_reps=1): - """Starts a generic BLE scan via the PMC app + + def __init__(self, android_device, music_file): + """Initialize the media_control class. + + Args: + android_dut: android_device object + music_file: location of the music file + """ + self.android_device = android_device + self.music_file = music_file + + def player_on_foreground(self): + """Turn on screen and make sure media play is on foreground + + All media control keycode only works when screen is on and media player + is on the foreground. Turn off screen first and turn it on to make sure + all operation is based on the same screen status. Otherwise, 'MENU' key + would block command to be sent. + """ + self.android_device.droid.goToSleepNow() + time.sleep(SCREEN_WAIT_TIME) + self.android_device.droid.wakeUpNow() + time.sleep(SCREEN_WAIT_TIME) + self.android_device.send_keycode('MENU') + time.sleep(SCREEN_WAIT_TIME) + + def play(self): + """Start playing music. + + """ + self.player_on_foreground() + PLAY = 'am start -a android.intent.action.VIEW -d file://{} -t audio/wav'.format( + self.music_file) + self.android_device.adb.shell(PLAY) + + def pause(self): + """Pause music. + + """ + self.player_on_foreground() + self.android_device.send_keycode('MEDIA_PAUSE') + + def resume(self): + """Pause music. + + """ + self.player_on_foreground() + self.android_device.send_keycode('MEDIA_PLAY') + + def stop(self): + """Stop music and close media play. + + """ + self.player_on_foreground() + self.android_device.send_keycode('MEDIA_STOP') + + +def start_apk_ble_adv(dut, adv_mode, adv_power_level, adv_duration): + """Trigger BLE advertisement from power-test.apk. Args: - dut: object of the android device under test - scan mode: desired BLE scan type - offset_start: Time delay in seconds before scan starts - scan_time: active scan time - idle_time: iddle time (i.e., no scans occuring) - num_reps: Number of repetions of the ative+idle scan sequence + dut: Android device under test, type AndroidDevice obj + adv_mode: The BLE advertisement mode. + {0: 'LowPower', 1: 'Balanced', 2: 'LowLatency'} + adv_power_leve: The BLE advertisement TX power level. + {0: 'UltraLowTXPower', 1: 'LowTXPower', 2: 'MediumTXPower, + 3: HighTXPower} + adv_duration: duration of advertisement in seconds, type int """ - scan_dur = scan_time - if not idle_time: - idle_time = 0.2 * scan_time - scan_dur = 0.8 * scan_time - - first_part_msg = '%s%s --es StartTime %d --es ScanTime %d' % ( - PMC_BASE_SCAN, scan_mode, offset_start, scan_dur) - msg = '%s --es NoScanTime %d --es Repetitions %d' % (first_part_msg, - idle_time, num_reps) + adv_duration = str(adv_duration) + 's' + builder = icb.InstrumentationTestCommandBuilder.default() + builder.add_test_class( + "com.google.android.device.power.tests.ble.BleAdvertise") + builder.set_manifest_package("com.google.android.device.power") + builder.set_runner("androidx.test.runner.AndroidJUnitRunner") + builder.add_key_value_param("cool-off-duration", "0s") + builder.add_key_value_param("idle-duration", "0s") + builder.add_key_value_param( + "com.android.test.power.receiver.ADVERTISE_MODE", adv_mode) + builder.add_key_value_param("com.android.test.power.receiver.POWER_LEVEL", + adv_power_level) + builder.add_key_value_param( + "com.android.test.power.receiver.ADVERTISING_DURATION", adv_duration) + + adv_command = builder.build() + ' &' + logging.info('Start BLE {} at {} for {} seconds'.format( + bleenum.AdvertiseSettingsAdvertiseMode(adv_mode).name, + bleenum.AdvertiseSettingsAdvertiseTxPower(adv_power_level).name, + adv_duration)) + dut.adb.shell_nb(adv_command) + + +def start_apk_ble_scan(dut, scan_mode, scan_duration): + """Build the command to trigger BLE scan from power-test.apk. - dut.log.info('Sent BLE scan broadcast message: %s', msg) - dut.adb.shell(msg) + Args: + dut: Android device under test, type AndroidDevice obj + scan_mode: The BLE scan mode. + {0: 'LowPower', 1: 'Balanced', 2: 'LowLatency', -1: 'Opportunistic'} + scan_duration: duration of scan in seconds, type int + Returns: + adv_command: the command for BLE scan + """ + scan_duration = str(scan_duration) + 's' + builder = icb.InstrumentationTestCommandBuilder.default() + builder.add_test_class("com.google.android.device.power.tests.ble.BleScan") + builder.set_manifest_package("com.google.android.device.power") + builder.set_runner("androidx.test.runner.AndroidJUnitRunner") + builder.add_key_value_param("cool-off-duration", "0s") + builder.add_key_value_param("idle-duration", "0s") + builder.add_key_value_param("com.android.test.power.receiver.SCAN_MODE", + scan_mode) + builder.add_key_value_param("com.android.test.power.receiver.MATCH_MODE", + 2) + builder.add_key_value_param( + "com.android.test.power.receiver.SCAN_DURATION", scan_duration) + builder.add_key_value_param( + "com.android.test.power.receiver.CALLBACK_TYPE", 1) + builder.add_key_value_param("com.android.test.power.receiver.FILTER", + 'true') + + scan_command = builder.build() + ' &' + logging.info('Start BLE {} scans for {} seconds'.format( + bleenum.ScanSettingsScanMode(scan_mode).name, scan_duration)) + dut.adb.shell_nb(scan_command) diff --git a/acts/framework/acts/test_utils/bt/bt_test_utils.py b/acts/framework/acts/test_utils/bt/bt_test_utils.py index 57992eb19f..da4a88089f 100644 --- a/acts/framework/acts/test_utils/bt/bt_test_utils.py +++ b/acts/framework/acts/test_utils/bt/bt_test_utils.py @@ -79,7 +79,8 @@ def _add_android_device_to_dictionary(android_device, profile_list, profile_list: The list of profiles the Android device supports. """ for profile in profile_list: - if profile in selector_dict and android_device not in selector_dict[profile]: + if profile in selector_dict and android_device not in selector_dict[ + profile]: selector_dict[profile].append(android_device) else: selector_dict[profile] = [android_device] @@ -164,7 +165,7 @@ def cleanup_scanners_and_advertisers(scn_android_device, scn_callback_list, except Exception as err: adv_android_device.log.debug( "Failed to stop LE advertisement... reseting Bluetooth. Error {}". - format(err)) + format(err)) reset_bluetooth([adv_android_device]) @@ -197,7 +198,9 @@ def clear_bonded_devices(ad): return True -def connect_phone_to_headset(android, headset, timeout=bt_default_timeout, +def connect_phone_to_headset(android, + headset, + timeout=bt_default_timeout, connection_check_period=10): """Connects android phone to bluetooth headset. Headset object must have methods power_on and enter_pairing_mode, @@ -214,7 +217,8 @@ def connect_phone_to_headset(android, headset, timeout=bt_default_timeout, connected (bool): True if devices are paired and connected by end of method. False otherwise. """ - connected = is_a2dp_src_device_connected(android, headset.mac_address) + headset_mac_address = headset.mac_address + connected = is_a2dp_src_device_connected(android, headset_mac_address) log.info('Devices connected before pair attempt: %s' % connected) if not connected: # Turn on headset and initiate pairing mode. @@ -224,15 +228,18 @@ def connect_phone_to_headset(android, headset, timeout=bt_default_timeout, # If already connected, skip pair and connect attempt. while not connected and (time.time() - start_time < timeout): bonded_info = android.droid.bluetoothGetBondedDevices() - if headset.mac_address not in [info["address"] for info in bonded_info]: + if headset.mac_address not in [ + info["address"] for info in bonded_info + ]: # Use SL4A to pair and connect with headset. - android.droid.bluetoothDiscoverAndBond(headset.mac_address) + headset.enter_pairing_mode() + android.droid.bluetoothDiscoverAndBond(headset_mac_address) else: # Device is bonded but not connected - android.droid.bluetoothConnectBonded(headset.mac_address) + android.droid.bluetoothConnectBonded(headset_mac_address) log.info('Waiting for connection...') time.sleep(connection_check_period) # Check for connection. - connected = is_a2dp_src_device_connected(android, headset.mac_address) + connected = is_a2dp_src_device_connected(android, headset_mac_address) log.info('Devices connected after pair attempt: %s' % connected) return connected @@ -393,12 +400,13 @@ def determine_max_advertisements(android_device): advertise_callback = android_device.droid.bleGenBleAdvertiseCallback() advertise_callback_list.append(advertise_callback) - android_device.droid.bleStartBleAdvertising( - advertise_callback, advertise_data, advertise_settings) + android_device.droid.bleStartBleAdvertising(advertise_callback, + advertise_data, + advertise_settings) regex = "(" + adv_succ.format( advertise_callback) + "|" + adv_fail.format( - advertise_callback) + ")" + advertise_callback) + ")" # wait for either success or failure event evt = android_device.ed.pop_events(regex, bt_default_timeout, small_timeout) @@ -584,10 +592,9 @@ def generate_ble_scan_objects(droid): return filter_list, scan_settings, scan_callback -def generate_id_by_size( - size, - chars=( - string.ascii_lowercase + string.ascii_uppercase + string.digits)): +def generate_id_by_size(size, + chars=(string.ascii_lowercase + + string.ascii_uppercase + string.digits)): """Generate random ascii characters of input size and input char types Args: @@ -649,6 +656,122 @@ def get_bluetooth_crash_count(android_device): return int(re.search("crashed(.*\d)", out).group(1)) +def get_bt_metric(ad_list, duration=1, tag="bt_metric", processed=True): + """ Function to get the bt metric from logcat. + + Captures logcat for the specified duration and returns the bqr results. + Takes list of android objects as input. If a single android object is given, + converts it into a list. + + Args: + ad_list: list of android_device objects + duration: time duration (seconds) for which the logcat is parsed. + tag: tag to be appended to the logcat dump. + processed: flag to process bqr output. + + Returns: + metrics_dict: dict of metrics for each android device. + """ + + # Defining bqr quantitites and their regex to extract + regex_dict = {"pwlv": "PwLv:\s(\S+)", "rssi": "RSSI:\s[-](\d+)"} + metrics_dict = {"rssi": {}, "pwlv": {}} + + # Converting a single android device object to list + if not isinstance(ad_list, list): + ad_list = [ad_list] + + #Time sync with the test machine + for ad in ad_list: + ad.droid.setTime(int(round(time.time() * 1000))) + time.sleep(0.5) + + begin_time = utils.get_current_epoch_time() + time.sleep(duration) + end_time = utils.get_current_epoch_time() + + for ad in ad_list: + bt_rssi_log = ad.cat_adb_log(tag, begin_time, end_time) + bqr_tag = "Monitoring , Handle:" + + # Extracting supporting bqr quantities + for metric, regex in regex_dict.items(): + bqr_metric = [] + file_bt_log = open(bt_rssi_log, "r") + for line in file_bt_log: + if bqr_tag in line: + if re.findall(regex, line): + m = re.findall(regex, line)[0].strip(",") + bqr_metric.append(m) + metrics_dict[metric][ad.serial] = bqr_metric + + # Formatting the raw data + metrics_dict["rssi"][ad.serial] = [ + (-1) * int(x) for x in metrics_dict["rssi"][ad.serial] + ] + metrics_dict["pwlv"][ad.serial] = [ + int(x, 16) for x in metrics_dict["pwlv"][ad.serial] + ] + + # Processing formatted data if processing is required + if processed: + # Computes the average RSSI + metrics_dict["rssi"][ad.serial] = round( + sum(metrics_dict["rssi"][ad.serial]) / + len(metrics_dict["rssi"][ad.serial]), 2) + # Returns last noted value for power level + metrics_dict["pwlv"][ad.serial] = metrics_dict["pwlv"][ + ad.serial][-1] + + return metrics_dict + + +def get_bt_rssi(ad, duration=1, processed=True): + """Function to get average bt rssi from logcat. + + This function returns the average RSSI for the given duration. RSSI values are + extracted from BQR. + + Args: + ad: (list of) android_device object. + duration: time duration(seconds) for which logcat is parsed. + + Returns: + avg_rssi: average RSSI on each android device for the given duration. + """ + function_tag = "get_bt_rssi" + bqr_results = get_bt_metric(ad, + duration, + tag=function_tag, + processed=processed) + return bqr_results["rssi"] + + +def enable_bqr(ad_list, bqr_interval=10, bqr_event_mask=15,): + """Sets up BQR reporting. + + Sets up BQR to report BT metrics at the requested frequency and toggles + airplane mode for the bqr settings to take effect. + + Args: + ad_list: an android_device or list of android devices. + """ + # Converting a single android device object to list + if not isinstance(ad_list, list): + ad_list = [ad_list] + + for ad in ad_list: + #Setting BQR parameters + ad.adb.shell("setprop persist.bluetooth.bqr.event_mask {}".format( + bqr_event_mask)) + ad.adb.shell("setprop persist.bluetooth.bqr.min_interval_ms {}".format( + bqr_interval)) + + ## Toggle airplane mode + ad.droid.connectivityToggleAirplaneMode(True) + ad.droid.connectivityToggleAirplaneMode(False) + + def get_device_selector_dictionary(android_device_list): """Create a dictionary of Bluetooth features vs Android devices. @@ -732,8 +855,8 @@ def get_mac_address_of_generic_advertisement(scan_ad, adv_ad): adv_ad.droid.bleStartBleAdvertising(advertise_callback, advertise_data, advertise_settings) try: - adv_ad.ed.pop_event( - adv_succ.format(advertise_callback), bt_default_timeout) + adv_ad.ed.pop_event(adv_succ.format(advertise_callback), + bt_default_timeout) except Empty as err: raise BtTestUtilsError( "Advertiser did not start successfully {}".format(err)) @@ -972,8 +1095,8 @@ def orchestrate_bluetooth_socket_connection( client_ad.droid.bluetoothStartPairingHelper() server_ad.droid.bluetoothSocketConnBeginAcceptThreadUuid( - (bluetooth_socket_conn_test_uuid - if uuid is None else uuid), accept_timeout_ms) + (bluetooth_socket_conn_test_uuid if uuid is None else uuid), + accept_timeout_ms) client_ad.droid.bluetoothSocketConnBeginConnectThreadUuid( server_ad.droid.bluetoothGetLocalAddress(), (bluetooth_socket_conn_test_uuid if uuid is None else uuid)) @@ -1037,12 +1160,14 @@ def pair_pri_to_sec(pri_ad, sec_ad, attempts=2, auto_confirm=True): # Wait 2 seconds before unbound time.sleep(2) if not clear_bonded_devices(pri_ad): - log.error("Failed to clear bond for primary device at attempt {}" - .format(str(curr_attempts))) + log.error( + "Failed to clear bond for primary device at attempt {}".format( + str(curr_attempts))) return False if not clear_bonded_devices(sec_ad): - log.error("Failed to clear bond for secondary device at attempt {}" - .format(str(curr_attempts))) + log.error( + "Failed to clear bond for secondary device at attempt {}". + format(str(curr_attempts))) return False # Wait 2 seconds after unbound time.sleep(2) @@ -1140,8 +1265,8 @@ def scan_and_verify_n_advertisements(scn_ad, max_advertisements): while (start_time + bt_default_timeout) > time.time(): event = None try: - event = scn_ad.ed.pop_event( - scan_result.format(scan_callback), bt_default_timeout) + event = scn_ad.ed.pop_event(scan_result.format(scan_callback), + bt_default_timeout) except Empty as error: raise BtTestUtilsError( "Failed to find scan event: {}".format(error)) @@ -1155,13 +1280,12 @@ def scan_and_verify_n_advertisements(scn_ad, max_advertisements): return test_result -def set_bluetooth_codec( - android_device, - codec_type, - sample_rate, - bits_per_sample, - channel_mode, - codec_specific_1=0): +def set_bluetooth_codec(android_device, + codec_type, + sample_rate, + bits_per_sample, + channel_mode, + codec_specific_1=0): """Sets the A2DP codec configuration on the AndroidDevice. Args: @@ -1180,31 +1304,26 @@ def set_bluetooth_codec( bool: True if the codec config was successfully changed to the desired values. Else False. """ - message = ( - "Set Android Device A2DP Bluetooth codec configuration:\n" - "\tCodec: {codec_type}\n" - "\tSample Rate: {sample_rate}\n" - "\tBits per Sample: {bits_per_sample}\n" - "\tChannel Mode: {channel_mode}".format( - codec_type=codec_type, - sample_rate=sample_rate, - bits_per_sample=bits_per_sample, - channel_mode=channel_mode - ) - ) + message = ("Set Android Device A2DP Bluetooth codec configuration:\n" + "\tCodec: {codec_type}\n" + "\tSample Rate: {sample_rate}\n" + "\tBits per Sample: {bits_per_sample}\n" + "\tChannel Mode: {channel_mode}".format( + codec_type=codec_type, + sample_rate=sample_rate, + bits_per_sample=bits_per_sample, + channel_mode=channel_mode)) android_device.log.info(message) # Send SL4A command droid, ed = android_device.droid, android_device.ed if not droid.bluetoothA2dpSetCodecConfigPreference( - codec_types[codec_type], - sample_rates[str(sample_rate)], - bits_per_samples[str(bits_per_sample)], - channel_modes[channel_mode], - codec_specific_1 - ): - android_device.log.warning("SL4A command returned False. Codec was not " - "changed.") + codec_types[codec_type], sample_rates[str(sample_rate)], + bits_per_samples[str(bits_per_sample)], + channel_modes[channel_mode], codec_specific_1): + android_device.log.warning( + "SL4A command returned False. Codec was not " + "changed.") else: try: ed.pop_event(bluetooth_a2dp_codec_config_changed, @@ -1222,14 +1341,10 @@ def set_bluetooth_codec( android_device.log.warning("Could not verify codec config change " "through ADB.") elif split_out[1].strip().upper() != codec_type: - android_device.log.error( - "Codec config was not changed.\n" - "\tExpected codec: {exp}\n" - "\tActual codec: {act}".format( - exp=codec_type, - act=split_out[1].strip() - ) - ) + android_device.log.error("Codec config was not changed.\n" + "\tExpected codec: {exp}\n" + "\tActual codec: {act}".format( + exp=codec_type, act=split_out[1].strip())) return False android_device.log.info("Bluetooth codec successfully changed.") return True @@ -1335,8 +1450,8 @@ def setup_multiple_devices_for_bt_test(android_devices): threads = [] try: for a in android_devices: - thread = threading.Thread( - target=factory_reset_bluetooth, args=([[a]])) + thread = threading.Thread(target=factory_reset_bluetooth, + args=([[a]])) threads.append(thread) thread.start() for t in threads: @@ -1390,8 +1505,8 @@ def setup_n_advertisements(adv_ad, num_advertisements): adv_ad.droid.bleStartBleAdvertising(advertise_callback, advertise_data, advertise_settings) try: - adv_ad.ed.pop_event( - adv_succ.format(advertise_callback), bt_default_timeout) + adv_ad.ed.pop_event(adv_succ.format(advertise_callback), + bt_default_timeout) adv_ad.log.info("Advertisement {} started.".format(i + 1)) except Empty as error: adv_ad.log.error("Advertisement {} failed to start.".format(i + 1)) @@ -1542,8 +1657,9 @@ def _wait_for_passkey_match(pri_ad, sec_ad): timeout=bt_default_timeout) sec_variant = sec_pairing_req["data"]["PairingVariant"] sec_pin = sec_pairing_req["data"]["Pin"] - sec_ad.log.info("Secondary device received Pin: {}, Variant: {}" - .format(sec_pin, sec_variant)) + sec_ad.log.info( + "Secondary device received Pin: {}, Variant: {}".format( + sec_pin, sec_variant)) except Empty as err: log.error("Wait for pin error: {}".format(err)) log.error("Pairing request state, Primary: {}, Secondary: {}".format( diff --git a/acts/framework/acts/test_utils/bt/pts/fuchsia_pts_ics_lib.py b/acts/framework/acts/test_utils/bt/pts/fuchsia_pts_ics_lib.py index 5ea32c1e68..f2f9b2c295 100644 --- a/acts/framework/acts/test_utils/bt/pts/fuchsia_pts_ics_lib.py +++ b/acts/framework/acts/test_utils/bt/pts/fuchsia_pts_ics_lib.py @@ -108,6 +108,56 @@ A2DP_ICS = { b'TSPC_A2DP_7a_3': b'FALSE', b'TSPC_A2DP_7b_1': b'FALSE', b'TSPC_A2DP_7b_2': b'FALSE', + + # Not available in Launch Studio Yet + b'TSPC_A2DP_10_1': b'FALSE', + b'TSPC_A2DP_10_2': b'FALSE', + b'TSPC_A2DP_10_3': b'FALSE', + b'TSPC_A2DP_10_4': b'FALSE', + b'TSPC_A2DP_10_5': b'FALSE', + b'TSPC_A2DP_10_6': b'FALSE', + b'TSPC_A2DP_11_1': b'FALSE', + b'TSPC_A2DP_11_2': b'FALSE', + b'TSPC_A2DP_11_3': b'FALSE', + b'TSPC_A2DP_11_4': b'FALSE', + b'TSPC_A2DP_11_5': b'FALSE', + b'TSPC_A2DP_11_6': b'FALSE', + b'TSPC_A2DP_12_2': b'FALSE', + b'TSPC_A2DP_12_3': b'FALSE', + b'TSPC_A2DP_12_3': b'FALSE', + b'TSPC_A2DP_12_4': b'FALSE', + b'TSPC_A2DP_13_1': b'FALSE', + b'TSPC_A2DP_13_2': b'FALSE', + b'TSPC_A2DP_13_3': b'FALSE', + b'TSPC_A2DP_13_4': b'FALSE', + b'TSPC_A2DP_14_1': b'FALSE', + b'TSPC_A2DP_14_2': b'FALSE', + b'TSPC_A2DP_14_3': b'FALSE', + b'TSPC_A2DP_14_4': b'FALSE', + b'TSPC_A2DP_14_5': b'FALSE', + b'TSPC_A2DP_15_1': b'FALSE', + b'TSPC_A2DP_15_2': b'FALSE', + b'TSPC_A2DP_15_3': b'FALSE', + b'TSPC_A2DP_15_4': b'FALSE', + b'TSPC_A2DP_15_5': b'FALSE', + b'TSPC_A2DP_15_6': b'FALSE', + b'TSPC_A2DP_3_2a': b'FALSE', + b'TSPC_A2DP_3_2b': b'FALSE', + b'TSPC_A2DP_3_2c': b'FALSE', + b'TSPC_A2DP_3_2d': b'FALSE', + b'TSPC_A2DP_3_2e': b'FALSE', + b'TSPC_A2DP_3_2f': b'FALSE', + b'TSPC_A2DP_5_2a': b'FALSE', + b'TSPC_A2DP_5_2b': b'FALSE', + b'TSPC_A2DP_5_2c': b'FALSE', + b'TSPC_A2DP_8_2': b'FALSE', + b'TSPC_A2DP_8_3': b'FALSE', + b'TSPC_A2DP_8_4': b'FALSE', + b'TSPC_A2DP_9_1': b'FALSE', + b'TSPC_A2DP_9_2': b'FALSE', + b'TSPC_A2DP_9_3': b'FALSE', + b'TSPC_A2DP_9_4': b'FALSE', + } diff --git a/acts/framework/acts/test_utils/bt/pts/pts_base_class.py b/acts/framework/acts/test_utils/bt/pts/pts_base_class.py index 568c092736..b2c48d66e1 100644 --- a/acts/framework/acts/test_utils/bt/pts/pts_base_class.py +++ b/acts/framework/acts/test_utils/bt/pts/pts_base_class.py @@ -121,8 +121,6 @@ class PtsBaseClass(BaseTestClass): # TODO: Implement MMIs as necessary } } - - self.pts.setup_pts() self.pts.bind_to(self.process_next_action) def teardown_class(self): diff --git a/acts/framework/acts/test_utils/gnss/gnss_test_utils.py b/acts/framework/acts/test_utils/gnss/gnss_test_utils.py index 2b4e6f3a72..641c8e5ad8 100644 --- a/acts/framework/acts/test_utils/gnss/gnss_test_utils.py +++ b/acts/framework/acts/test_utils/gnss/gnss_test_utils.py @@ -84,9 +84,6 @@ def reboot(ad): if not int(ad.adb.shell("settings get global mobile_data")) == 1: set_mobile_data(ad, True) utils.sync_device_time(ad) - if ad.model == "sailfish" or ad.model == "marlin": - remount_device(ad) - ad.adb.shell("echo at@test=8 >> /dev/at_mdm0") def enable_gnss_verbose_logging(ad): """Enable GNSS VERBOSE Logging and persistent logcat. @@ -99,6 +96,7 @@ def enable_gnss_verbose_logging(ad): ad.adb.shell("echo DEBUG_LEVEL = 5 >> /vendor/etc/gps.conf") ad.adb.shell("echo %r >> /data/local.prop" % LOCAL_PROP_FILE_CONTENTS) ad.adb.shell("chmod 644 /data/local.prop") + ad.adb.shell("setprop persist.logd.logpersistd.size 20000") ad.adb.shell("setprop persist.logd.size 16777216") ad.adb.shell("setprop persist.vendor.radio.adb_log_on 1") ad.adb.shell("setprop persist.logd.logpersistd logcatd") @@ -504,22 +502,29 @@ def clear_aiding_data_by_gtw_gpstool(ad): ad.adb.shell("am start -S -n com.android.gpstool/.GPSTool --es mode clear") time.sleep(10) -def start_gnss_by_gtw_gpstool(ad, state, type="gnss"): + +def start_gnss_by_gtw_gpstool(ad, state, type="gnss", bgdisplay=False): """Start or stop GNSS on GTW_GPSTool. Args: ad: An AndroidDevice object. state: True to start GNSS. False to Stop GNSS. type: Different API for location fix. Use gnss/flp/nmea + bgdisplay: true to run GTW when Display off. + false to not run GTW when Display off. """ - if state: + if state and not bgdisplay: ad.adb.shell("am start -S -n com.android.gpstool/.GPSTool " "--es mode gps --es type %s" % type) + elif state and bgdisplay: + ad.adb.shell("am start -S -n com.android.gpstool/.GPSTool --es mode" + " gps --es type {} --ez BG {}".format(type, bgdisplay)) if not state: ad.log.info("Stop %s on GTW_GPSTool." % type) ad.adb.shell("am broadcast -a com.android.gpstool.stop_gps_action") time.sleep(3) + def process_gnss_by_gtw_gpstool(ad, criteria, type="gnss"): """Launch GTW GPSTool and Clear all GNSS aiding data Start GNSS tracking on GTW_GPSTool. @@ -807,7 +812,10 @@ def ttff_property_key_and_value(ad, ttff_data, ttff_mode): pe_list = [float(ttff_data[key].ttff_pe) for key in ttff_data.keys()] cn_list = [float(ttff_data[key].ttff_cn) for key in ttff_data.keys()] timeoutcount = sec_list.count(0.0) - avgttff = sum(sec_list)/(len(sec_list) - timeoutcount) + if len(sec_list) == timeoutcount: + avgttff = 9527 + else: + avgttff = sum(sec_list)/(len(sec_list) - timeoutcount) if timeoutcount != 0: maxttff = 9527 else: @@ -1021,6 +1029,7 @@ def get_baseband_and_gms_version(ad, extra_msg=""): ad: An AndroidDevice object. """ try: + build_version = ad.adb.getprop("ro.build.id") baseband_version = ad.adb.getprop("gsm.version.baseband") gms_version = ad.adb.shell( "dumpsys package com.google.android.gms | grep versionName" @@ -1028,6 +1037,7 @@ def get_baseband_and_gms_version(ad, extra_msg=""): mpss_version = ad.adb.shell("cat /sys/devices/soc0/images | grep MPSS " "| cut -d ':' -f 3") if not extra_msg: + ad.log.info("TestResult Build_Version %s" % build_version) ad.log.info("TestResult Baseband_Version %s" % baseband_version) ad.log.info( "TestResult GMS_Version %s" % gms_version.replace(" ", "")) diff --git a/acts/framework/acts/test_utils/gnss/gnss_testlog_utils.py b/acts/framework/acts/test_utils/gnss/gnss_testlog_utils.py new file mode 100644 index 0000000000..6bb18dfc1a --- /dev/null +++ b/acts/framework/acts/test_utils/gnss/gnss_testlog_utils.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +# +# Copyright 2019 - The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Python Module for GNSS test log utilities.''' + +import re as regex +import datetime +import functools as fts +import numpy as npy +import pandas as pds +from acts import logger + +# GPS API Log Reading Config +CONFIG_GPSAPILOG = { + 'phone_time': + r'(?P<date>\d+\/\d+\/\d+)\s+(?P<time>\d+:\d+:\d+)\s+' + r'Read:\s+(?P<logsize>\d+)\s+bytes', + 'SpaceVehicle': + r'Fix:\s+(?P<Fix>\w+)\s+Type:\s+(?P<Type>\w+)\s+' + r'SV:\s+(?P<SV>\d+)\s+C\/No:\s+(?P<CNo>\d+\.\d+)\s+' + r'Elevation:\s+(?P<Elevation>\d+\.\d+)\s+' + r'Azimuth:\s+(?P<Azimuth>\d+\.\d+)\s+' + r'Signal:\s+(?P<Signal>\w+)\s+' + r'Frequency:\s+(?P<Frequency>\d+\.\d+)\s+' + r'EPH:\s+(?P<EPH>\w+)\s+ALM:\s+(?P<ALM>\w+)', + 'HistoryAvgTop4CNo': + r'History\s+Avg\s+Top4\s+:\s+(?P<HistoryAvgTop4CNo>\d+\.\d+)', + 'CurrentAvgTop4CNo': + r'Current\s+Avg\s+Top4\s+:\s+(?P<CurrentAvgTop4CNo>\d+\.\d+)', + 'HistoryAvgCNo': + r'History\s+Avg\s+:\s+(?P<HistoryAvgCNo>\d+\.\d+)', + 'CurrentAvgCNo': + r'Current\s+Avg\s+:\s+(?P<CurrentAvgCNo>\d+\.\d+)', + 'L5inFix': + r'L5\s+used\s+in\s+fix:\s+(?P<L5inFix>\w+)', + 'L5EngagingRate': + r'L5\s+engaging\s+rate:\s+(?P<L5EngagingRate>\d+.\d+)%', + 'Provider': + r'Provider:\s+(?P<Provider>\w+)', + 'Latitude': + r'Latitude:\s+(?P<Latitude>-?\d+.\d+)', + 'Longitude': + r'Longitude:\s+(?P<Longitude>-?\d+.\d+)', + 'Altitude': + r'Altitude:\s+(?P<Altitude>-?\d+.\d+)', + 'GNSSTime': + r'Time:\s+(?P<Date>\d+\/\d+\/\d+)\s+' + r'(?P<Time>\d+:\d+:\d+)', + 'Speed': + r'Speed:\s+(?P<Speed>\d+.\d+)', + 'Bearing': + r'Bearing:\s+(?P<Bearing>\d+.\d+)', +} + +# Space Vehicle Statistics Dataframe List +LIST_SVSTAT = [ + 'HistoryAvgTop4CNo', 'CurrentAvgTop4CNo', 'HistoryAvgCNo', 'CurrentAvgCNo', + 'L5inFix', 'L5EngagingRate' +] + +# Location Fix Info Dataframe List +LIST_LOCINFO = [ + 'Provider', 'Latitude', 'Longitude', 'Altitude', 'GNSSTime', 'Speed', + 'Bearing' +] + +LOGPARSE_UTIL_LOGGER = logger.create_logger() + + +def parse_log_to_df(filename, configs, index_rownum=True): + r"""Parse log to a dictionary of Pandas dataframes. + + Args: + filename: log file name. + Type String. + configs: configs dictionary of parsed Pandas dataframes. + Type dictionary. + dict key, the parsed pattern name, such as 'Speed', + dict value, regex of the config pattern, + Type Raw String. + index_rownum: index row number from raw data. + Type Boolean. + Default, True. + + Returns: + parsed_data: dictionary of parsed data. + Type dictionary. + dict key, the parsed pattern name, such as 'Speed', + dict value, the corresponding parsed dataframe. + + Examples: + configs = { + 'GNSSTime': + r'Time:\s+(?P<Date>\d+\/\d+\/\d+)\s+ + r(?P<Time>\d+:\d+:\d+)')}, + 'Speed': r'Speed:\s+(?P<Speed>\d+.\d+)', + } + """ + # Init a local config dictionary to hold compiled regex and match dict. + configs_local = {} + # Construct parsed data dictionary + parsed_data = {} + + # Loop the config dictionary to compile regex and init data list + for key, regex_string in configs.items(): + configs_local[key] = { + 'cregex': regex.compile(regex_string), + 'datalist': [], + } + + # Open the file, loop and parse + with open(filename, 'r') as fid: + + for idx_line, current_line in enumerate(fid): + for _, config in configs_local.items(): + matched_log_object = config['cregex'].search(current_line) + + if matched_log_object: + matched_data = matched_log_object.groupdict() + matched_data['rownumber'] = idx_line + 1 + config['datalist'].append(matched_data) + + # Loop to generate parsed data from configs list + for key, config in configs_local.items(): + parsed_data[key] = pds.DataFrame(config['datalist']) + if index_rownum and not parsed_data[key].empty: + parsed_data[key].set_index('rownumber', inplace=True) + elif parsed_data[key].empty: + LOGPARSE_UTIL_LOGGER.warning( + 'The parsed dataframe of "%s" is empty.', key) + + # Return parsed data list + return parsed_data + + +def parse_gpsapilog_to_df(filename): + """Parse GPS API log to Pandas dataframes. + + Args: + filename: full log file name. + Type, String. + + Returns: + timestamp_df: Timestamp Data Frame. + Type, Pandas DataFrame. + sv_info_df: GNSS SV info Data Frame. + Type, Pandas DataFrame. + loc_info_df: Location Information Data Frame. + Type, Pandas DataFrame. + include Provider, Latitude, Longitude, Altitude, GNSSTime, Speed, Bearing + """ + + def get_phone_time(target_df_row, timestamp_df): + """subfunction to get the phone_time.""" + + try: + row_num = timestamp_df[ + timestamp_df.index < target_df_row.name].iloc[-1].name + phone_time = timestamp_df.loc[row_num]['phone_time'] + except IndexError: + row_num = npy.NaN + phone_time = npy.NaN + + return phone_time, row_num + + # Get parsed dataframe list + parsed_data = parse_log_to_df( + filename=filename, + configs=CONFIG_GPSAPILOG, + ) + + # get DUT Timestamp + timestamp_df = parsed_data['phone_time'] + timestamp_df['phone_time'] = timestamp_df.apply( + lambda row: datetime.datetime.strptime(row.date + '-' + row.time, + '%Y/%m/%d-%H:%M:%S'), + axis=1) + + # Add phone_time from timestamp_df dataframe by row number + for key in parsed_data: + if key != 'phone_time': + current_df = parsed_data[key] + time_n_row_num = current_df.apply(get_phone_time, + axis=1, + timestamp_df=timestamp_df) + current_df[['phone_time', 'time_row_num' + ]] = pds.DataFrame(time_n_row_num.apply(pds.Series)) + + # Get space vehicle info dataframe + sv_info_df = parsed_data['SpaceVehicle'] + + # Get space vehicle statistics dataframe + # First merge all dataframe from LIST_SVSTAT[1:], + # Drop duplicated 'phone_time', based on time_row_num + sv_stat_df = fts.reduce( + lambda item1, item2: pds.merge(item1, item2, on='time_row_num'), [ + parsed_data[key].drop(['phone_time'], axis=1) + for key in LIST_SVSTAT[1:] + ]) + # Then merge with LIST_SVSTAT[0] + sv_stat_df = pds.merge(sv_stat_df, + parsed_data[LIST_SVSTAT[0]], + on='time_row_num') + + # Get location fix information dataframe + # First merge all dataframe from LIST_LOCINFO[1:], + # Drop duplicated 'phone_time', based on time_row_num + loc_info_df = fts.reduce( + lambda item1, item2: pds.merge(item1, item2, on='time_row_num'), [ + parsed_data[key].drop(['phone_time'], axis=1) + for key in LIST_LOCINFO[1:] + ]) + # Then merge with LIST_LOCINFO[8] + loc_info_df = pds.merge(loc_info_df, + parsed_data[LIST_LOCINFO[0]], + on='time_row_num') + # Convert GNSS Time + loc_info_df['gnsstime'] = loc_info_df.apply( + lambda row: datetime.datetime.strptime(row.Date + '-' + row.Time, + '%Y/%m/%d-%H:%M:%S'), + axis=1) + + return timestamp_df, sv_info_df, sv_stat_df, loc_info_df + + +def parse_gpsapilog_to_df_v2(filename): + """Parse GPS API log to Pandas dataframes, by using merge_asof. + + Args: + filename: full log file name. + Type, String. + + Returns: + timestamp_df: Timestamp Data Frame. + Type, Pandas DataFrame. + sv_info_df: GNSS SV info Data Frame. + Type, Pandas DataFrame. + loc_info_df: Location Information Data Frame. + Type, Pandas DataFrame. + include Provider, Latitude, Longitude, Altitude, GNSSTime, Speed, Bearing + """ + # Get parsed dataframe list + parsed_data = parse_log_to_df( + filename=filename, + configs=CONFIG_GPSAPILOG, + ) + + # get DUT Timestamp + timestamp_df = parsed_data['phone_time'] + timestamp_df['phone_time'] = timestamp_df.apply( + lambda row: datetime.datetime.strptime(row.date + '-' + row.time, + '%Y/%m/%d-%H:%M:%S'), + axis=1) + # drop logsize, date, time + parsed_data['phone_time'] = timestamp_df.drop(['logsize', 'date', 'time'], + axis=1) + + # Add phone_time from timestamp dataframe by row number + for key in parsed_data: + if key != 'phone_time': + parsed_data[key] = pds.merge_asof(parsed_data[key], + parsed_data['phone_time'], + left_index=True, + right_index=True) + + # Get space vehicle info dataframe + sv_info_df = parsed_data['SpaceVehicle'] + + # Get space vehicle statistics dataframe + # First merge all dataframe from LIST_SVSTAT[1:], + sv_stat_df = fts.reduce( + lambda item1, item2: pds.merge(item1, item2, on='phone_time'), + [parsed_data[key] for key in LIST_SVSTAT[1:]]) + # Then merge with LIST_SVSTAT[0] + sv_stat_df = pds.merge(sv_stat_df, + parsed_data[LIST_SVSTAT[0]], + on='phone_time') + + # Get location fix information dataframe + # First merge all dataframe from LIST_LOCINFO[1:], + loc_info_df = fts.reduce( + lambda item1, item2: pds.merge(item1, item2, on='phone_time'), + [parsed_data[key] for key in LIST_LOCINFO[1:]]) + # Then merge with LIST_LOCINFO[8] + loc_info_df = pds.merge(loc_info_df, + parsed_data[LIST_LOCINFO[0]], + on='phone_time') + # Convert GNSS Time + loc_info_df['gnsstime'] = loc_info_df.apply( + lambda row: datetime.datetime.strptime(row.Date + '-' + row.Time, + '%Y/%m/%d-%H:%M:%S'), + axis=1) + + return timestamp_df, sv_info_df, sv_stat_df, loc_info_df diff --git a/acts/framework/acts/test_utils/power/PowerBTBaseTest.py b/acts/framework/acts/test_utils/power/PowerBTBaseTest.py index 5dfda12c7a..e0fd37da2e 100644 --- a/acts/framework/acts/test_utils/power/PowerBTBaseTest.py +++ b/acts/framework/acts/test_utils/power/PowerBTBaseTest.py @@ -14,18 +14,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import time +import os +import acts.test_utils.bt.bt_power_test_utils as btputils +import acts.test_utils.bt.bt_test_utils as btutils import acts.test_utils.power.PowerBaseTest as PBT -from acts.test_utils.bt.bt_test_utils import enable_bluetooth -from acts.test_utils.bt.bt_test_utils import disable_bluetooth +from acts.test_utils.abstract_devices.bluetooth_handsfree_abstract_device import BluetoothHandsfreeAbstractDeviceFactory as bt_factory -BT_BASE_UUID = '00000000-0000-1000-8000-00805F9B34FB' -BT_CLASSICAL_DATA = [1, 2, 3] -BLE_LOCATION_SCAN_ENABLE = 'settings put global ble_scan_always_enabled 1' -BLE_LOCATION_SCAN_DISABLE = 'settings put global ble_scan_always_enabled 0' -START_PMC_CMD = 'am start -n com.android.pmc/com.android.pmc.PMCMainActivity' -PMC_VERBOSE_CMD = 'setprop log.tag.PMC VERBOSE' -PMC_BASE_SCAN = 'am broadcast -a com.android.pmc.BLESCAN --es ScanMode ' +BLE_LOCATION_SCAN_DISABLE = 'settings put secure location_mode 0' +PHONE_MUSIC_FILE_DIRECTORY = '/sdcard/Music' +INIT_ATTEN = [30] class PowerBTBaseTest(PBT.PowerBaseTest): @@ -34,16 +31,43 @@ class PowerBTBaseTest(PBT.PowerBaseTest): Inherited from the PowerBaseTest class """ + def setup_class(self): + + super().setup_class() + # Get music file and push it to the phone + music_files = self.user_params.get('music_files', []) + if music_files: + music_src = music_files[0] + music_dest = PHONE_MUSIC_FILE_DIRECTORY + success = self.dut.push_system_file(music_src, music_dest) + if success: + self.music_file = os.path.join(PHONE_MUSIC_FILE_DIRECTORY, + os.path.basename(music_src)) + # Initialize media_control class + self.media = btputils.MediaControl(self.dut, self.music_file) + # Set Attenuator to the initial attenuation + if hasattr(self, 'attenuators'): + self.set_attenuation(INIT_ATTEN) + # Create the BTOE(Bluetooth-Other-End) device object + bt_devices = self.user_params.get('bt_devices', []) + if bt_devices: + attr, idx = bt_devices.split(':') + self.bt_device_controller = getattr(self, attr)[int(idx)] + self.bt_device = bt_factory().generate(self.bt_device_controller) + else: + self.log.error('No BT devices config is provided!') + # Turn off screen as all tests will be screen off + self.dut.droid.goToSleepNow() + def setup_test(self): super().setup_test() + self.unpack_userparams(volume=0.9) # Reset BT to factory defaults self.dut.droid.bluetoothFactoryReset() - time.sleep(2) - # Start PMC app. - self.log.info('Start PMC app...') - self.dut.adb.shell(START_PMC_CMD) - self.dut.adb.shell(PMC_VERBOSE_CMD) + self.bt_device.reset() + self.bt_device.power_on() + btutils.enable_bluetooth(self.dut.droid, self.dut.ed) def teardown_test(self): """Tear down necessary objects after test case is finished. @@ -54,6 +78,11 @@ class PowerBTBaseTest(PBT.PowerBaseTest): super().teardown_test() self.dut.droid.bluetoothFactoryReset() self.dut.adb.shell(BLE_LOCATION_SCAN_DISABLE) + if hasattr(self, 'media'): + self.media.stop() + self.bt_device.reset() + self.bt_device.power_off() + btutils.disable_bluetooth(self.dut.droid) def teardown_class(self): """Clean up the test class after tests finish running @@ -61,75 +90,7 @@ class PowerBTBaseTest(PBT.PowerBaseTest): """ super().teardown_class() self.dut.droid.bluetoothFactoryReset() - - def phone_setup_for_BT(self, bt_on, ble_on, screen_status): - """Sets the phone and Bluetooth in the desired state - - Args: - bt_on: Enable/Disable BT - ble_on: Enable/Disable BLE - screen_status: screen ON or OFF - """ - - # Check if we are enabling a background scan - # TODO: Turn OFF cellular wihtout having to turn ON airplane mode - if bt_on == 'OFF' and ble_on == 'ON': - self.dut.adb.shell(BLE_LOCATION_SCAN_ENABLE) - self.dut.droid.connectivityToggleAirplaneMode(False) - time.sleep(2) - - # Turn ON/OFF BT - if bt_on == 'ON': - enable_bluetooth(self.dut.droid, self.dut.ed) - self.dut.log.info('BT is ON') - else: - disable_bluetooth(self.dut.droid) - self.dut.droid.bluetoothDisableBLE() - self.dut.log.info('BT is OFF') - time.sleep(2) - - # Turn ON/OFF BLE - if ble_on == 'ON': - self.dut.droid.bluetoothEnableBLE() - self.dut.log.info('BLE is ON') - else: - self.dut.droid.bluetoothDisableBLE() - self.dut.log.info('BLE is OFF') - time.sleep(2) - - # Set the desired screen status - if screen_status == 'OFF': - self.dut.droid.goToSleepNow() - self.dut.log.info('Screen is OFF') - time.sleep(2) - - def start_pmc_ble_scan(self, - scan_mode, - offset_start, - scan_time, - idle_time=None, - num_reps=1): - """Starts a generic BLE scan via the PMC app - - Args: - dut: object of the android device under test - scan mode: desired BLE scan type - offset_start: Time delay in seconds before scan starts - scan_time: active scan time - idle_time: iddle time (i.e., no scans occuring) - num_reps: Number of repetions of the ative+idle scan sequence - """ - scan_dur = scan_time - if not idle_time: - idle_time = 0.2 * scan_time - scan_dur = 0.8 * scan_time - - first_part_msg = '%s%s --es StartTime %d --es ScanTime %d' % ( - PMC_BASE_SCAN, scan_mode, offset_start, scan_dur) - - msg = '%s --es NoScanTime %d --es Repetitions %d' % (first_part_msg, - idle_time, - num_reps) - - self.dut.log.info('Sent BLE scan broadcast message: %s', msg) - self.dut.adb.shell(msg) + self.dut.adb.shell(BLE_LOCATION_SCAN_DISABLE) + self.bt_device.reset() + self.bt_device.power_off() + btutils.disable_bluetooth(self.dut.droid) diff --git a/acts/framework/acts/test_utils/power/PowerBaseTest.py b/acts/framework/acts/test_utils/power/PowerBaseTest.py index 92fc603209..2334702828 100644 --- a/acts/framework/acts/test_utils/power/PowerBaseTest.py +++ b/acts/framework/acts/test_utils/power/PowerBaseTest.py @@ -17,39 +17,42 @@ import json import logging import math import os +import re import time + import acts.controllers.iperf_server as ipf from acts import asserts from acts import base_test from acts import utils -from acts.controllers import monsoon +from acts.controllers.monsoon_lib.api.common import MonsoonError +from acts.controllers.monsoon_lib.api.common import PassthroughStates from acts.metrics.loggers.blackbox import BlackboxMetricLogger from acts.test_utils.power.loggers.power_metric_logger import PowerMetricLogger -from acts.test_utils.wifi import wifi_test_utils as wutils from acts.test_utils.wifi import wifi_power_test_utils as wputils +from acts.test_utils.wifi import wifi_test_utils as wutils RESET_BATTERY_STATS = 'dumpsys batterystats --reset' IPERF_TIMEOUT = 180 -THRESHOLD_TOLERANCE = 0.2 +THRESHOLD_TOLERANCE_DEFAULT = 0.2 GET_FROM_PHONE = 'get_from_dut' GET_FROM_AP = 'get_from_ap' -PHONE_BATTERY_VOLTAGE = 4.2 +PHONE_BATTERY_VOLTAGE_DEFAULT = 4.2 MONSOON_MAX_CURRENT = 8.0 MONSOON_RETRY_INTERVAL = 300 +DEFAULT_MONSOON_FREQUENCY = 500 MEASUREMENT_RETRY_COUNT = 3 RECOVER_MONSOON_RETRY_COUNT = 3 MIN_PERCENT_SAMPLE = 95 ENABLED_MODULATED_DTIM = 'gEnableModulatedDTIM=' MAX_MODULATED_DTIM = 'gMaxLIModulatedDTIM=' TEMP_FILE = '/sdcard/Download/tmp.log' -IPERF_DURATION = 'iperf_duration' -INITIAL_ATTEN = [0, 0, 90, 90] class ObjNew(): """Create a random obj with unknown attributes and value. """ + def __init__(self, **kwargs): self.__dict__.update(kwargs) @@ -68,6 +71,7 @@ class PowerBaseTest(base_test.BaseTestClass): """Base class for all wireless power related tests. """ + def __init__(self, controllers): base_test.BaseTestClass.__init__(self, controllers) @@ -83,17 +87,38 @@ class PowerBaseTest(base_test.BaseTestClass): self.log = logging.getLogger() self.tests = self._get_all_test_names() + # Obtain test parameters from user_params + TEST_PARAMS = self.TAG + '_params' + self.test_params = self.user_params.get(TEST_PARAMS, {}) + if not self.test_params: + self.log.warning(TEST_PARAMS + ' was not found in the user ' + 'parameters defined in the config file.') + + # Override user_param values with test parameters + self.user_params.update(self.test_params) + + # Unpack user_params with default values. All the usages of user_params + # as self attributes need to be included either as a required parameter + # or as a parameter with a default value. + req_params = ['custom_files', 'mon_duration'] + self.unpack_userparams(req_params, + mon_freq=DEFAULT_MONSOON_FREQUENCY, + mon_offset=0, + bug_report=False, + extra_wait=None, + iperf_duration=None, + pass_fail_tolerance=THRESHOLD_TOLERANCE_DEFAULT, + mon_voltage=PHONE_BATTERY_VOLTAGE_DEFAULT) + # Setup the must have controllers, phone and monsoon self.dut = self.android_devices[0] self.mon_data_path = os.path.join(self.log_path, 'Monsoon') + os.makedirs(self.mon_data_path, exist_ok=True) self.mon = self.monsoons[0] self.mon.set_max_current(8.0) - self.mon.set_voltage(PHONE_BATTERY_VOLTAGE) + self.mon.set_voltage(self.mon_voltage) self.mon.attach_device(self.dut) - # Unpack the test/device specific parameters - req_params = ['custom_files'] - self.unpack_userparams(req_params) # Unpack the custom files based on the test configs for file in self.custom_files: if 'pass_fail_threshold_' + self.dut.model in file: @@ -104,7 +129,7 @@ class PowerBaseTest(base_test.BaseTestClass): self.network_file = file elif 'rockbottom_' + self.dut.model in file: self.rockbottom_script = file - #Abort the class if threshold and rockbottom file is missing + # Abort the class if threshold and rockbottom file is missing asserts.abort_class_if( not self.threshold_file, 'Required test pass/fail threshold file is missing') @@ -112,19 +137,14 @@ class PowerBaseTest(base_test.BaseTestClass): not self.rockbottom_script, 'Required rockbottom setting script is missing') - # Unpack test specific configs - TEST_PARAMS = self.TAG + '_params' - self.test_params = self.user_params.get(TEST_PARAMS, {}) - self.unpack_testparams(self.test_params) if hasattr(self, 'attenuators'): self.num_atten = self.attenuators[0].instrument.num_atten self.atten_level = self.unpack_custom_file(self.attenuation_file) - self.set_attenuation(INITIAL_ATTEN) self.threshold = self.unpack_custom_file(self.threshold_file) self.mon_info = self.create_monsoon_info() # Sync device time, timezone and country code - utils.require_sl4a((self.dut, )) + utils.require_sl4a((self.dut,)) utils.sync_device_time(self.dut) self.dut.droid.wifiSetCountryCode('US') @@ -148,7 +168,7 @@ class PowerBaseTest(base_test.BaseTestClass): wutils.wifi_toggle_state(self.dut, False) # Wait for extra time if needed for the first test - if hasattr(self, 'extra_wait'): + if self.extra_wait: self.more_wait_first_test() def teardown_test(self): @@ -194,20 +214,13 @@ class PowerBaseTest(base_test.BaseTestClass): # Restart SL4A self.dut.start_services() - def unpack_testparams(self, bulk_params): - """Unpack all the test specific parameters. - - Args: - bulk_params: dict with all test specific params in the config file - """ - for key in bulk_params.keys(): - setattr(self, key, bulk_params[key]) - def unpack_custom_file(self, file, test_specific=True): """Unpack the pass_fail_thresholds from a common file. Args: file: the common file containing pass fail threshold. + test_specific: if True, returns the JSON element within the file + that starts with the test class name. """ with open(file, 'r') as f: params = json.load(f) @@ -255,21 +268,23 @@ class PowerBaseTest(base_test.BaseTestClass): """The actual test flow and result processing and validate. """ - self.collect_power_data() - self.pass_fail_check() + result = self.collect_power_data() + self.pass_fail_check(result.average_current) def collect_power_data(self): """Measure power, plot and take log if needed. + Returns: + A MonsoonResult object. """ - tag = '' # Collecting current measurement data and plot - self.file_path, self.test_result = self.monsoon_data_collect_save() - self.power_result.metric_value = (self.test_result * - PHONE_BATTERY_VOLTAGE) - wputils.monsoon_data_plot(self.mon_info, self.file_path, tag=tag) + result = self.monsoon_data_collect_save() + self.power_result.metric_value = (result.average_current * + self.mon_voltage) + wputils.monsoon_data_plot(self.mon_info, result) + return result - def pass_fail_check(self): + def pass_fail_check(self, average_current=None): """Check the test result and decide if it passed or failed. The threshold is provided in the config file. In this class, result is @@ -282,18 +297,18 @@ class PowerBaseTest(base_test.BaseTestClass): return current_threshold = self.threshold[self.test_name] - if self.test_result: + if average_current: asserts.assert_true( - abs(self.test_result - current_threshold) / current_threshold < - THRESHOLD_TOLERANCE, + abs(average_current - current_threshold) / current_threshold < + self.pass_fail_tolerance, 'Measured average current in [{}]: {:.2f}mA, which is ' 'out of the acceptable range {:.2f}±{:.2f}mA'.format( - self.test_name, self.test_result, current_threshold, + self.test_name, average_current, current_threshold, self.pass_fail_tolerance * current_threshold)) asserts.explicit_pass( 'Measurement finished for [{}]: {:.2f}mA, which is ' 'within the acceptable range {:.2f}±{:.2f}'.format( - self.test_name, self.test_result, current_threshold, + self.test_name, average_current, current_threshold, self.pass_fail_tolerance * current_threshold)) else: asserts.fail( @@ -305,7 +320,7 @@ class PowerBaseTest(base_test.BaseTestClass): Returns: mon_info: Dictionary with the monsoon packet config """ - if hasattr(self, IPERF_DURATION): + if self.iperf_duration: self.mon_duration = self.iperf_duration - 10 mon_info = ObjNew(dut=self.mon, freq=self.mon_freq, @@ -330,8 +345,12 @@ class PowerBaseTest(base_test.BaseTestClass): logging.info('Monsoon recovered from unexpected error') time.sleep(2) return True - except monsoon.MonsoonError: - logging.info(self.mon.mon.ser.in_waiting) + except MonsoonError: + try: + self.log.info(self.mon_info.dut._mon.ser.in_waiting) + except AttributeError: + # This attribute does not exist for HVPMs. + pass logging.warning('Unable to recover monsoon from unexpected error') return False @@ -342,94 +361,70 @@ class PowerBaseTest(base_test.BaseTestClass): log file. Take bug report if requested. Returns: - data_path: the absolute path to the log file of monsoon current - measurement - avg_current: the average current of the test + A MonsoonResult object containing information about the gathered + data. """ tag = '{}_{}_{}'.format(self.test_name, self.dut.model, self.dut.build_info['build_id']) + data_path = os.path.join(self.mon_info.data_path, '{}.txt'.format(tag)) - total_expected_samples = self.mon_info.freq * (self.mon_info.duration + - self.mon_info.offset) - min_required_samples = total_expected_samples * MIN_PERCENT_SAMPLE / 100 - # Retry counter for monsoon data aquisition - retry_measure = 1 - # Indicator that need to re-collect data - need_collect_data = 1 - result = None - while retry_measure <= MEASUREMENT_RETRY_COUNT: - try: - # If need to retake data - if need_collect_data == 1: - #Resets the battery status right before the test started - self.dut.adb.shell(RESET_BATTERY_STATS) - self.log.info( - 'Starting power measurement with monsoon box, try #{}'. - format(retry_measure)) - #Start the power measurement using monsoon - self.mon_info.dut.monsoon_usb_auto() - result = self.mon_info.dut.measure_power( - self.mon_info.freq, - self.mon_info.duration, - tag=tag, - offset=self.mon_info.offset) - self.mon_info.dut.reconnect_dut() - # Reconnect to dut - else: - self.mon_info.dut.reconnect_dut() - # Reconnect and return measurement results if no error happens - avg_current = result.average_current - monsoon.MonsoonData.save_to_text_file([result], data_path) - self.log.info('Power measurement done within {} try'.format( + + # If the specified Monsoon data file already exists (e.g., multiple + # measurements in a single test), write the results to a new file with + # the postfix "_#". + if os.path.exists(data_path): + highest_value = 1 + for filename in os.listdir(os.path.dirname(data_path)): + match = re.match(r'{}_(\d+).txt'.format(tag), filename) + if match: + highest_value = int(match.group(1)) + + data_path = os.path.join(self.mon_info.data_path, + '%s_%s.txt' % (tag, highest_value + 1)) + + total_expected_samples = self.mon_info.freq * self.mon_info.duration + min_required_samples = (total_expected_samples + * MIN_PERCENT_SAMPLE / 100) + for retry_measure in range(1, MEASUREMENT_RETRY_COUNT + 1): + # Resets the battery status right before the test starts. + self.dut.adb.shell(RESET_BATTERY_STATS) + self.log.info( + 'Starting power measurement, attempt #{}.'.format( retry_measure)) - return data_path, avg_current - # Catch monsoon errors during measurement - except monsoon.MonsoonError: - self.log.info(self.mon_info.dut.mon.ser.in_waiting) - # Break early if it's one count away from limit - if retry_measure == MEASUREMENT_RETRY_COUNT: - self.log.error( - 'Test failed after maximum measurement retry') - break - - self.log.warning('Monsoon error happened, now try to recover') - # Retry loop to recover monsoon from error - retry_monsoon = 1 - while retry_monsoon <= RECOVER_MONSOON_RETRY_COUNT: - mon_status = self.monsoon_recover() - if mon_status: - break - else: - retry_monsoon += 1 - self.log.warning( - 'Wait for {} second then try again'.format( - MONSOON_RETRY_INTERVAL)) - time.sleep(MONSOON_RETRY_INTERVAL) - - # Break the loop to end test if failed to recover monsoon - if not mon_status: - self.log.error( - 'Tried our best, still failed to recover monsoon') - break - else: - # If there is no data, or captured samples are less than min - # required, re-take - if not result: - self.log.warning('No data taken, need to remeasure') - elif len(result._data_points) <= min_required_samples: - self.log.warning( - 'More than {} percent of samples are missing due to monsoon error. Need to remeasure' - .format(100 - MIN_PERCENT_SAMPLE)) - else: - need_collect_data = 0 - self.log.warning( - 'Data collected is valid, try reconnect to DUT to finish test' - ) - retry_measure += 1 - - if retry_measure > MEASUREMENT_RETRY_COUNT: - self.log.error('Test failed after maximum measurement retry') + # Start the power measurement using monsoon. + self.mon_info.dut.usb(PassthroughStates.AUTO) + result = self.mon_info.dut.measure_power( + self.mon_info.duration, + measure_after_seconds=self.mon_info.offset, + hz=self.mon_info.freq, + output_path=data_path) + self.mon_info.dut.usb(PassthroughStates.ON) + + self.log.debug(result) + self.log.debug('Samples Gathered: %s. Max Samples: %s ' + 'Min Samples Required: %s.' % + (result.num_samples, total_expected_samples, + min_required_samples)) + + if result.num_samples <= min_required_samples: + retry_measure += 1 + self.log.warning( + 'More than {} percent of samples are missing due to ' + 'dropped packets. Need to remeasure.'.format( + 100 - MIN_PERCENT_SAMPLE)) + continue + + self.log.info('Measurement successful after {} attempt(s).'.format( + retry_measure)) + return result + else: + try: + self.log.info(self.mon_info.dut._mon.ser.in_waiting) + except AttributeError: + # This attribute does not exist for HVPMs. + pass + self.log.error('Unable to gather enough samples to run validation.') def process_iperf_results(self): """Get the iperf results and process. @@ -454,7 +449,7 @@ class PowerBaseTest(base_test.BaseTestClass): throughput = (math.fsum( iperf_result.instantaneous_rates[self.start_meas_time:-1] ) / len(iperf_result.instantaneous_rates[self.start_meas_time:-1]) - ) * 8 * (1.024**2) + ) * 8 * (1.024 ** 2) self.log.info('The average throughput is {}'.format(throughput)) except ValueError: diff --git a/acts/framework/acts/test_utils/power/PowerCellularLabBaseTest.py b/acts/framework/acts/test_utils/power/PowerCellularLabBaseTest.py index d7bc63cfdc..d3dae158e8 100644 --- a/acts/framework/acts/test_utils/power/PowerCellularLabBaseTest.py +++ b/acts/framework/acts/test_utils/power/PowerCellularLabBaseTest.py @@ -76,6 +76,12 @@ class PowerCellularLabBaseTest(PBT.PowerBaseTest): super().setup_class() + # Unpack test parameters used in this class + self.unpack_userparams(md8475_version=None, + md8475a_ip_address=None, + cmw500_ip=None, + cmw500_port=None) + # Load calibration tables filename_calibration_table = ( self.FILENAME_CALIBRATION_TABLE_UNFORMATTED.format( @@ -114,12 +120,12 @@ class PowerCellularLabBaseTest(PBT.PowerBaseTest): False if a connection with the callbox could not be started """ - if hasattr(self, 'md8475_version'): + if self.md8475_version: self.log.info('Selecting Anrtisu MD8475 callbox.') # Verify the callbox IP address has been indicated in the configs - if not hasattr(self, 'md8475_version'): + if not self.md8475a_ip_address: raise RuntimeError( 'md8475a_ip_address was not included in the test ' 'configuration.') @@ -132,7 +138,7 @@ class PowerCellularLabBaseTest(PBT.PowerBaseTest): else: raise ValueError('Invalid MD8475 version.') - elif hasattr(self, 'cmw500_ip') or hasattr(self, 'cmw500_port'): + elif self.cmw500_ip or self.cmw500_port: for key in ['cmw500_ip', 'cmw500_port']: if not hasattr(self, key): diff --git a/acts/framework/acts/test_utils/power/PowerGnssBaseTest.py b/acts/framework/acts/test_utils/power/PowerGnssBaseTest.py index 14c95975c9..5f38aec086 100644 --- a/acts/framework/acts/test_utils/power/PowerGnssBaseTest.py +++ b/acts/framework/acts/test_utils/power/PowerGnssBaseTest.py @@ -17,6 +17,8 @@ import logging import time +import os + import acts.test_utils.power.PowerBaseTest as PBT from acts import base_test @@ -38,40 +40,50 @@ class PowerGnssBaseTest(PBT.PowerBaseTest): """ def collect_power_data(self): - """Measure power and plot. - - """ - tag = '_Power' - super().collect_power_data() - self.monsoon_data_plot_power(self.mon_info, self.file_path, tag=tag) + """Measure power and plot.""" + result = super().collect_power_data() + self.monsoon_data_plot_power(self.mon_info, result, tag='_Power') + return result - def monsoon_data_plot_power(self, mon_info, file_path, tag=""): + def monsoon_data_plot_power(self, mon_info, monsoon_results, tag=''): """Plot the monsoon power data using bokeh interactive plotting tool. Args: - mon_info: Dictionary with the monsoon packet config - file_path: the path to the monsoon log file with current data + mon_info: Dictionary with the monsoon packet config. + monsoon_results: a MonsoonResult or list of MonsoonResult objects to + to plot. + tag: an extra tag to append to the resulting filename. """ - self.log.info("Plot the power measurement data") - # Get results as monsoon data object from the input file - results = monsoon.MonsoonData.from_text_file(file_path) - # Decouple current and timestamp data from the monsoon object - current_data = [] - timestamps = [] - voltage = results[0].voltage - for result in results: - current_data.extend(result.data_points) - timestamps.extend(result.timestamps) - period = 1 / mon_info.freq - time_relative = [x * period for x in range(len(current_data))] - # Calculate the average current for the test - - current_data = [x * 1000 for x in current_data] - power_data = [x * voltage for x in current_data] - avg_current = sum(current_data) / len(current_data) - color = ['navy'] * len(current_data) + if not isinstance(monsoon_results, list): + monsoon_results = [monsoon_results] + logging.info('Plotting the power measurement data.') + + voltage = monsoon_results[0].voltage + + total_current = 0 + total_samples = 0 + for result in monsoon_results: + total_current += result.average_current * result.num_samples + total_samples += result.num_samples + avg_current = total_current / total_samples + + time_relative = [ + data_point.time + for monsoon_result in monsoon_results + for data_point in monsoon_result.get_data_points() + ] + + power_data = [ + data_point.current * voltage + for monsoon_result in monsoon_results + for data_point in monsoon_result.get_data_points() + ] + + total_data_points = sum( + result.num_samples for result in monsoon_results) + color = ['navy'] * total_data_points # Preparing the data and source link for bokehn java callback source = ColumnDataSource( @@ -94,18 +106,20 @@ class PowerGnssBaseTest(PBT.PowerBaseTest): dt = DataTable( source=s2, columns=columns, width=1300, height=60, editable=True) - plot_title = file_path[file_path.rfind('/') + 1:-4] + tag - output_file("%s/%s.html" % (mon_info.data_path, plot_title)) - TOOLS = 'box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save' + plot_title = (os.path.basename( + os.path.splitext(monsoon_results[0].tag)[0]) + + tag) + output_file(os.path.join(mon_info.data_path, plot_title + '.html')) + tools = 'box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save' # Create a new plot with the datatable above plot = figure( plot_width=1300, plot_height=700, title=plot_title, - tools=TOOLS, - output_backend="webgl") - plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="width")) - plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="height")) + tools=tools, + output_backend='webgl') + plot.add_tools(bokeh_tools.WheelZoomTool(dimensions='width')) + plot.add_tools(bokeh_tools.WheelZoomTool(dimensions='height')) plot.line('x0', 'y0', source=source, line_width=2) plot.circle('x0', 'y0', source=source, size=0.5, fill_color='color') plot.xaxis.axis_label = 'Time (s)' @@ -119,8 +133,7 @@ class PowerGnssBaseTest(PBT.PowerBaseTest): code=customjsscript) # Layout the plot and the datatable bar - l = layout([[dt], [plot]]) - save(l) + save(layout([[dt], [plot]])) def disconnect_usb(self, ad, sleeptime): """Disconnect usb while device is on sleep and diff --git a/acts/framework/acts/test_utils/power/PowerWiFiBaseTest.py b/acts/framework/acts/test_utils/power/PowerWiFiBaseTest.py index e357ae953b..937656e352 100644 --- a/acts/framework/acts/test_utils/power/PowerWiFiBaseTest.py +++ b/acts/framework/acts/test_utils/power/PowerWiFiBaseTest.py @@ -19,6 +19,7 @@ from acts.test_utils.wifi import wifi_test_utils as wutils from acts.test_utils.wifi import wifi_power_test_utils as wputils IPERF_DURATION = 'iperf_duration' +INITIAL_ATTEN = [0, 0, 90, 90] class PowerWiFiBaseTest(PBT.PowerBaseTest): @@ -35,6 +36,8 @@ class PowerWiFiBaseTest(PBT.PowerBaseTest): self.access_point_main = self.access_points[0] if len(self.access_points) > 1: self.access_point_aux = self.access_points[1] + if hasattr(self, 'attenuators'): + self.set_attenuation(INITIAL_ATTEN) if hasattr(self, 'network_file'): self.networks = self.unpack_custom_file(self.network_file, False) self.main_network = self.networks['main_network'] @@ -43,7 +46,7 @@ class PowerWiFiBaseTest(PBT.PowerBaseTest): self.pkt_sender = self.packet_senders[0] if hasattr(self, 'iperf_servers'): self.iperf_server = self.iperf_servers[0] - if hasattr(self, 'iperf_duration'): + if self.iperf_duration: self.mon_duration = self.iperf_duration - 10 self.create_monsoon_info() @@ -112,10 +115,11 @@ class PowerWiFiBaseTest(PBT.PowerBaseTest): If IPERF is run, need to pull iperf results and attach it to the plot. """ - super().collect_power_data() + result = super().collect_power_data() tag = '' - if hasattr(self, IPERF_DURATION): + if self.iperf_duration: throughput = self.process_iperf_results() tag = '_RSSI_{0:d}dBm_Throughput_{1:.2f}Mbps'.format( self.RSSI, throughput) - wputils.monsoon_data_plot(self.mon_info, self.file_path, tag=tag) + wputils.monsoon_data_plot(self.mon_info, result, tag=tag) + return result diff --git a/acts/framework/acts/test_utils/power/tel_simulations/LteSimulation.py b/acts/framework/acts/test_utils/power/tel_simulations/LteSimulation.py index c0cf063107..8637b2dcee 100644 --- a/acts/framework/acts/test_utils/power/tel_simulations/LteSimulation.py +++ b/acts/framework/acts/test_utils/power/tel_simulations/LteSimulation.py @@ -17,7 +17,6 @@ import math from enum import Enum -from acts.controllers.anritsu_lib import md8475_cellular_simulator as anritsusim from acts.test_utils.power.tel_simulations.BaseSimulation import BaseSimulation from acts.test_utils.tel.tel_defines import NETWORK_MODE_LTE_ONLY @@ -429,11 +428,6 @@ class LteSimulation(BaseSimulation): super().__init__(simulator, log, dut, test_config, calibration_table) - # The LTE simulation relies on the cellular simulator to be a MD8475 - if not isinstance(self.simulator, anritsusim.MD8475CellularSimulator): - raise ValueError('The LTE simulation relies on the simulator to ' - 'be an Anritsu MD8475 A/B instrument.') - if not dut.droid.telephonySetPreferredNetworkTypesForSubscription( NETWORK_MODE_LTE_ONLY, dut.droid.subscriptionGetDefaultSubId()): @@ -651,12 +645,10 @@ class LteSimulation(BaseSimulation): self.log.info( "The test name does not include the '{}' parameter. Disabled " "by default.".format(self.PARAM_RRC_STATUS_CHANGE_TIMER)) - self.simulator.anritsu.set_lte_rrc_status_change(False) + self.simulator.set_lte_rrc_state_change_timer(False) else: - self.rrc_sc_timer = int(values[1]) - self.simulator.anritsu.set_lte_rrc_status_change(True) - self.simulator.anritsu.set_lte_rrc_status_change_timer( - self.rrc_sc_timer) + timer = int(values[1]) + self.simulator.anritsu.set_lte_rrc_status_change(True, timer) # Get uplink power diff --git a/acts/framework/acts/test_utils/tel/tel_test_utils.py b/acts/framework/acts/test_utils/tel/tel_test_utils.py index 0607fc1112..323e1bc1e0 100644 --- a/acts/framework/acts/test_utils/tel/tel_test_utils.py +++ b/acts/framework/acts/test_utils/tel/tel_test_utils.py @@ -24,6 +24,7 @@ import re import os import urllib.parse import time +import acts.controllers.iperf_server as ipf from acts import signals from acts import utils @@ -551,9 +552,14 @@ def toggle_airplane_mode_by_adb(log, ad, new_state=None): elif new_state is None: new_state = not cur_state ad.log.info("Change airplane mode from %s to %s", cur_state, new_state) - ad.adb.shell("settings put global airplane_mode_on %s" % int(new_state)) - ad.adb.shell("am broadcast -a android.intent.action.AIRPLANE_MODE") - return True + try: + ad.adb.shell("settings put global airplane_mode_on %s" % int(new_state)) + ad.adb.shell("am broadcast -a android.intent.action.AIRPLANE_MODE") + except Exception as e: + ad.log.error(e) + return False + changed_state = bool(int(ad.adb.shell("settings get global airplane_mode_on"))) + return changed_state == new_state def toggle_airplane_mode(log, ad, new_state=None, strict_checking=True): @@ -796,6 +802,15 @@ def get_service_state_by_adb(log, ad): ad.log.info("mVoiceRegState is %s %s", result.group(1), result.group(2)) return result.group(2) + else: + if getattr(ad, "sdm_log", False): + #look for all occurrence in string + result2 = re.findall(r"mVoiceRegState=(\S+)\((\S+)\)", output) + for voice_state in result2: + if voice_state[0] == 0: + ad.log.info("mVoiceRegState is 0 %s", voice_state[1]) + return voice_state[1] + return result2[1][1] else: result = re.search(r"mServiceState=(\S+)", output) if result: @@ -2800,38 +2815,33 @@ def verify_internet_connection(log, ad, retries=3, expected_state=True): return False -def iperf_test_by_adb(log, - ad, - iperf_server, - port_num=None, - reverse=False, - timeout=180, - limit_rate=None, - omit=10, - ipv6=False, - rate_dict=None, - blocking=True, - log_file_path=None): - """Iperf test by adb. +def iperf_test_with_options(log, + ad, + iperf_server, + iperf_option, + timeout=180, + rate_dict=None, + blocking=True, + log_file_path=None): + """Iperf adb run helper. Args: log: log object ad: Android Device Object. - iperf_Server: The iperf host url". - port_num: TCP/UDP server port + iperf_server: The iperf host url". + iperf_option: The options to pass to iperf client timeout: timeout for file download to complete. - limit_rate: iperf bandwidth option. None by default - omit: the omit option provided in iperf command. + rate_dict: dictionary that can be passed in to save data + blocking: run iperf in blocking mode if True + log_file_path: location to save logs + Returns: + True if IPerf runs without throwing an exception """ - iperf_option = "-t %s -O %s -J" % (timeout, omit) - if limit_rate: iperf_option += " -b %s" % limit_rate - if port_num: iperf_option += " -p %s" % port_num - if ipv6: iperf_option += " -6" - if reverse: iperf_option += " -R" try: if log_file_path: ad.adb.shell("rm %s" % log_file_path, ignore_status=True) ad.log.info("Running adb iperf test with server %s", iperf_server) + ad.log.info("IPerf options are %s", iperf_option) if not blocking: ad.run_iperf_client_nb( iperf_server, @@ -2841,21 +2851,133 @@ def iperf_test_by_adb(log, return True result, data = ad.run_iperf_client( iperf_server, iperf_option, timeout=timeout + 60) - ad.log.info("Iperf test result with server %s is %s", iperf_server, + ad.log.info("IPerf test result with server %s is %s", iperf_server, result) if result: - data_json = json.loads(''.join(data)) - tx_rate = data_json['end']['sum_sent']['bits_per_second'] - rx_rate = data_json['end']['sum_received']['bits_per_second'] - ad.log.info( - 'iPerf3 upload speed is %sbps, download speed is %sbps', - tx_rate, rx_rate) + iperf_str = ''.join(data) + iperf_result = ipf.IPerfResult(iperf_str) + if "-u" in iperf_option: + udp_rate = iperf_result.avg_rate + if udp_rate is None: + ad.log.warning( + "UDP rate is none, IPerf server returned error: %s", + iperf_result.error) + ad.log.info("IPerf3 udp speed is %sbps", udp_rate) + else: + tx_rate = iperf_result.avg_send_rate + rx_rate = iperf_result.avg_receive_rate + if (tx_rate or rx_rate) is None: + ad.log.warning( + "A TCP rate is none, IPerf server returned error: %s", + iperf_result.error) + ad.log.info( + "IPerf3 upload speed is %sbps, download speed is %sbps", + tx_rate, rx_rate) if rate_dict is not None: rate_dict["Uplink"] = tx_rate rate_dict["Downlink"] = rx_rate return result - except Exception as e: + except AdbError as e: ad.log.warning("Fail to run iperf test with exception %s", e) + raise + + +def iperf_udp_test_by_adb(log, + ad, + iperf_server, + port_num=None, + reverse=False, + timeout=180, + limit_rate=None, + omit=10, + ipv6=False, + rate_dict=None, + blocking=True, + log_file_path=None): + """Iperf test by adb using UDP. + + Args: + log: log object + ad: Android Device Object. + iperf_Server: The iperf host url". + port_num: TCP/UDP server port + reverse: whether to test download instead of upload + timeout: timeout for file download to complete. + limit_rate: iperf bandwidth option. None by default + omit: the omit option provided in iperf command. + ipv6: whether to run the test as ipv6 + rate_dict: dictionary that can be passed in to save data + blocking: run iperf in blocking mode if True + log_file_path: location to save logs + """ + iperf_option = "-u -i 1 -t %s -O %s -J" % (timeout, omit) + if limit_rate: + iperf_option += " -b %s" % limit_rate + if port_num: + iperf_option += " -p %s" % port_num + if ipv6: + iperf_option += " -6" + if reverse: + iperf_option += " -R" + try: + return iperf_test_with_options(log, + ad, + iperf_server, + iperf_option, + timeout, + rate_dict, + blocking, + log_file_path) + except AdbError: + return False + +def iperf_test_by_adb(log, + ad, + iperf_server, + port_num=None, + reverse=False, + timeout=180, + limit_rate=None, + omit=10, + ipv6=False, + rate_dict=None, + blocking=True, + log_file_path=None): + """Iperf test by adb using TCP. + + Args: + log: log object + ad: Android Device Object. + iperf_server: The iperf host url". + port_num: TCP/UDP server port + reverse: whether to test download instead of upload + timeout: timeout for file download to complete. + limit_rate: iperf bandwidth option. None by default + omit: the omit option provided in iperf command. + ipv6: whether to run the test as ipv6 + rate_dict: dictionary that can be passed in to save data + blocking: run iperf in blocking mode if True + log_file_path: location to save logs + """ + iperf_option = "-t %s -O %s -J" % (timeout, omit) + if limit_rate: + iperf_option += " -b %s" % limit_rate + if port_num: + iperf_option += " -p %s" % port_num + if ipv6: + iperf_option += " -6" + if reverse: + iperf_option += " -R" + try: + return iperf_test_with_options(log, + ad, + iperf_server, + iperf_option, + timeout, + rate_dict, + blocking, + log_file_path) + except AdbError: return False diff --git a/acts/framework/acts/test_utils/wifi/ota_chamber.py b/acts/framework/acts/test_utils/wifi/ota_chamber.py index 26abbe5f8b..53494dad52 100644 --- a/acts/framework/acts/test_utils/wifi/ota_chamber.py +++ b/acts/framework/acts/test_utils/wifi/ota_chamber.py @@ -14,6 +14,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import contextlib +import io import time from acts import logger from acts import utils @@ -48,7 +50,6 @@ class OtaChamber(object): Base class provides functions whose implementation is shared by all chambers. """ - def reset_chamber(self): """Resets the chamber to its zero/home state.""" raise NotImplementedError @@ -80,7 +81,6 @@ class OtaChamber(object): class MockChamber(OtaChamber): """Class that implements mock chamber for test development and debug.""" - def __init__(self, config): self.config = config.copy() self.device_id = self.config['device_id'] @@ -117,7 +117,6 @@ class MockChamber(OtaChamber): class OctoboxChamber(OtaChamber): """Class that implements Octobox chamber.""" - def __init__(self, config): self.config = config.copy() self.device_id = self.config['device_id'] @@ -130,8 +129,9 @@ class OctoboxChamber(OtaChamber): def set_orientation(self, orientation): self.log.info('Setting orientation to {} degrees.'.format(orientation)) - utils.exe_cmd('sudo {} -d {} -p {}'.format( - self.TURNTABLE_FILE_PATH, self.device_id, orientation)) + utils.exe_cmd('sudo {} -d {} -p {}'.format(self.TURNTABLE_FILE_PATH, + self.device_id, + orientation)) def reset_chamber(self): self.log.info('Resetting chamber to home state') @@ -155,7 +155,6 @@ class ChamberAutoConnect(object): class BluetestChamber(OtaChamber): """Class that implements Octobox chamber.""" - def __init__(self, config): import flow self.config = config.copy() @@ -165,6 +164,16 @@ class BluetestChamber(OtaChamber): self.stirrer_ids = [0, 1, 2] self.current_mode = None + # Capture print output decorator + @staticmethod + def _capture_output(func, *args, **kwargs): + """Creates a decorator to capture stdout from bluetest module""" + f = io.StringIO() + with contextlib.redirect_stdout(f): + func(*args, **kwargs) + output = f.getvalue() + return output + def _connect(self): self.chamber.connect(self.config['ip_address'], self.config['username'], self.config['password']) @@ -172,12 +181,15 @@ class BluetestChamber(OtaChamber): def _init_manual_mode(self): self.current_mode = 'manual' for stirrer_id in self.stirrer_ids: - self.chamber.chamber_stirring_manual_init(stirrer_id) + out = self._capture_output( + self.chamber.chamber_stirring_manual_init, stirrer_id) + if "failed" in out: + self.log.warning("Initialization error: {}".format(out)) time.sleep(CHAMBER_SLEEP) def _init_continuous_mode(self): self.current_mode = 'continuous' - self.chamber.chamber_stirring_continous_init() + self.chamber.chamber_stirring_continuous_init() def _init_stepped_mode(self, steps): self.current_mode = 'stepped' @@ -188,7 +200,17 @@ class BluetestChamber(OtaChamber): if self.current_mode != 'manual': self._init_manual_mode() self.log.info('Setting stirrer {} to {}.'.format(stirrer_id, position)) - self.chamber.chamber_stirring_manual_set_pos(stirrer_id, position) + out = self._capture_output( + self.chamber.chamber_stirring_manual_set_pos, stirrer_id, position) + if "failed" in out: + self.log.warning("Bluetest error: {}".format(out)) + self.log.warning("Set position failed. Retrying.") + self.current_mode = None + self.set_stirrer_pos(stirrer_id, position) + else: + self._capture_output(self.chamber.chamber_stirring_manual_wait, + CHAMBER_SLEEP) + self.log.warning('Stirrer {} at {}.'.format(stirrer_id, position)) def set_orientation(self, orientation): self.set_stirrer_pos(2, orientation * 100 / 360) @@ -196,10 +218,10 @@ class BluetestChamber(OtaChamber): def start_continuous_stirrers(self): if self.current_mode != 'continuous': self._init_continuous_mode() - self.chamber.chamber_stirring_continous_start() + self.chamber.chamber_stirring_continuous_start() def stop_continuous_stirrers(self): - self.chamber.chamber_stirring_continous_stop() + self.chamber.chamber_stirring_continuous_stop() def step_stirrers(self, steps): if self.current_mode != 'stepped': diff --git a/acts/framework/acts/test_utils/wifi/ota_sniffer.py b/acts/framework/acts/test_utils/wifi/ota_sniffer.py index 264fd36819..884f8f12ca 100644 --- a/acts/framework/acts/test_utils/wifi/ota_sniffer.py +++ b/acts/framework/acts/test_utils/wifi/ota_sniffer.py @@ -1,23 +1,49 @@ -from acts import logger -from acts.controllers.utils_lib import ssh +#!/usr/bin/env python3 +# +# Copyright 2019 - The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import csv -import io import os +from acts import context +from acts import logger +from acts import utils +from acts.controllers.utils_lib import ssh -def create(configs, logging_dir): - """Factory method for sniffer +def create(configs): + """Factory method for sniffer. Args: - configs: list of dicts with sniffer settings, settings must contain the following : - ssh_settings (ssh credentials required to log into the sniffer) + configs: list of dicts with sniffer settings. + Settings must contain the following : ssh_settings, type, OS, interface. + + Returns: + objs: list of sniffer class objects. """ objs = [] for config in configs: try: if config["type"] == "tshark": - objs.append(TsharkSniffer(config, logging_dir)) + if config["os"] == "unix": + objs.append(TsharkSnifferOnUnix(config)) + elif config["os"] == "linux": + objs.append(TsharkSnifferOnLinux(config)) + else: + raise RuntimeError("Wrong sniffer config") + elif config["type"] == "mock": - objs.append(MockSniffer(config, logging_dir)) + objs.append(MockSniffer(config)) except KeyError: raise KeyError("Invalid sniffer configurations") return objs @@ -28,343 +54,431 @@ def destroy(objs): class OtaSnifferBase(object): - """Base class provides functions whose implementation in shared by all sniffers""" + """Base class defining common sniffers functions.""" _log_file_counter = 0 - def start_capture(self): - """Starts the sniffer Capture""" - raise NotImplementedError + @property + def started(self): + raise NotImplementedError('started must be specified.') - def stop_capture(self): - """Stops the sniffer Capture""" - raise NotImplementedError + def start_capture(self, network, duration=30): + """Starts the sniffer Capture. + + Args: + network: dict containing network information such as SSID, etc. + duration: duration of sniffer capture in seconds. + """ + raise NotImplementedError('start_capture must be specified.') + + def stop_capture(self, tag=""): + """Stops the sniffer Capture. + + Args: + tag: string to tag sniffer capture file name with. + """ + raise NotImplementedError('stop_capture must be specified.') def _get_remote_dump_path(self): + """Returns name of the sniffer dump file.""" return "sniffer_dump.csv" def _get_full_file_path(self, tag=None): """Returns the full file path for the sniffer capture dump file. - Returns the full file path (on test machine) for the sniffer capture dump file + Returns the full file path (on test machine) for the sniffer capture + dump file. Args: tag: The tag appended to the sniffer capture dump file . """ - out_dir = os.path.join(self.log_dir, "sniffer_files") - if not os.path.exists(out_dir): - os.mkdir(out_dir) tags = [tag, "count", OtaSnifferBase._log_file_counter] out_file_name = 'Sniffer_Capture_%s.csv' % ('_'.join( [str(x) for x in tags if x != '' and x is not None])) OtaSnifferBase._log_file_counter += 1 - file_path = os.path.join(out_dir, out_file_name) + file_path = os.path.join(self.log_path, out_file_name) return file_path + @property + def log_path(self): + current_context = context.get_current_context() + full_out_dir = os.path.join(current_context.get_full_output_path(), + 'sniffer_captures') -class MockSniffer(OtaSnifferBase): - """Class that implements mock sniffer for test development and debug""" + # Ensure the directory exists. + utils.create_dir(full_out_dir) - def __init__(self, config, logging_dir): - self.log = logger.create_tagged_trace_logger("Mock Sniffer") - self.log_dir = logging_dir + return full_out_dir - def _ssh_into_sniffer(self): - """logs into the sniffer machine""" - self.log.info("Logging into the sniffer machine") - def _connect_to_network(self): - """ Connects to a given network +class MockSniffer(OtaSnifferBase): + """Class that implements mock sniffer for test development and debug.""" + def __init__(self, config): + self.log = logger.create_tagged_trace_logger("Mock Sniffer") + + def start_capture(self, network, duration=30): + """Starts sniffer capture on the specified machine. Args: - network: dictionary of network credentials; SSID and password + network: dict of network credentials. + duration: duration of the sniff. """ - self.log.info("Connecting to wireless network ") - - def _run_sniffer(self): - """Starts the sniffer""" - self.log.info("Starting Sniffer") - self.log.info( - "Executing sniffer command on the sniffer machine") - - def _stop_sniffer(self): - """ Stops the sniffer""" - self.log.info("Stopping Sniffer") - - def start_capture(self): - """Starts sniffer capture on the specified machine""" - - self._ssh_into_sniffer() - self._connect_to_network() - self._run_sniffer() + self.log.info("Starting sniffer.") def stop_capture(self): - """Stops the sniffer + """Stops the sniffer. Returns: - The name of the processed sniffer dump from the terminated sniffer process + log_file: name of processed sniffer. """ - self._stop_sniffer() - log_file = self._get_full_file_path("Mock") + self.log.info("Stopping sniffer.") + log_file = self._get_full_file_path() + with open(log_file, 'w') as file: + file.write('this is a sniffer dump.') return log_file -class TsharkSniffer(OtaSnifferBase): - """Class that implements Tshark based Sniffer """ - - def __init__(self, config, logging_dir): +class TsharkSnifferBase(OtaSnifferBase): + """Class that implements Tshark based sniffer controller. """ + + TYPE_SUBTYPE_DICT = { + "0": "Association Requests", + "1": "Association Responses", + "2": "Reassociation Requests", + "3": "Resssociation Responses", + "4": "Probe Requests", + "5": "Probe Responses", + "8": "Beacon", + "9": "ATIM", + "10": "Disassociations", + "11": "Authentications", + "12": "Deauthentications", + "13": "Actions", + "24": "Block ACK Requests", + "25": "Block ACKs", + "26": "PS-Polls", + "27": "RTS", + "28": "CTS", + "29": "ACK", + "30": "CF-Ends", + "31": "CF-Ends/CF-Acks", + "32": "Data", + "33": "Data+CF-Ack", + "34": "Data+CF-Poll", + "35": "Data+CF-Ack+CF-Poll", + "36": "Null", + "37": "CF-Ack", + "38": "CF-Poll", + "39": "CF-Ack+CF-Poll", + "40": "QoS Data", + "41": "QoS Data+CF-Ack", + "42": "QoS Data+CF-Poll", + "43": "QoS Data+CF-Ack+CF-Poll", + "44": "QoS Null", + "46": "QoS CF-Poll (Null)", + "47": "QoS CF-Ack+CF-Poll (Null)" + } + + TSHARK_COLUMNS = [ + "frame_number", "frame_time_relative", "mactime", "frame_len", "rssi", + "channel", "ta", "ra", "bssid", "type", "subtype", "duration", "seq", + "retry", "pwrmgmt", "moredata", "ds", "phy", "radio_datarate", + "vht_datarate", "radiotap_mcs_index", "vht_mcs", "wlan_data_rate", + "11n_mcs_index", "11ac_mcs", "11n_bw", "11ac_bw", "vht_nss", "mcs_gi", + "vht_gi", "vht_coding", "ba_bm", "fc_status", "bf_report" + ] + + TSHARK_OUTPUT_COLUMNS = [ + "frame_number", "frame_time_relative", "mactime", "ta", "ra", "bssid", + "rssi", "channel", "frame_len", "Info", "radio_datarate", + "radiotap_mcs_index", "pwrmgmt", "phy", "vht_nss", "vht_mcs", + "vht_datarate", "11ac_mcs", "11ac_bw", "vht_gi", "vht_coding", + "wlan_data_rate", "11n_mcs_index", "11n_bw", "mcs_gi", "type", + "subtype", "duration", "seq", "retry", "moredata", "ds", "ba_bm", + "fc_status", "bf_report" + ] + + TSHARK_FIELDS_LIST = [ + 'frame.number', 'frame.time_relative', 'radiotap.mactime', 'frame.len', + 'radiotap.dbm_antsignal', 'wlan_radio.channel', 'wlan.ta', 'wlan.ra', + 'wlan.bssid', 'wlan.fc.type', 'wlan.fc.type_subtype', 'wlan.duration', + 'wlan.seq', 'wlan.fc.retry', 'wlan.fc.pwrmgt', 'wlan.fc.moredata', + 'wlan.fc.ds', 'wlan_radio.phy', 'radiotap.datarate', + 'radiotap.vht.datarate.0', 'radiotap.mcs.index', 'radiotap.vht.mcs.0', + 'wlan_radio.data_rate', 'wlan_radio.11n.mcs_index', + 'wlan_radio.11ac.mcs', 'wlan_radio.11n.bandwidth', + 'wlan_radio.11ac.bandwidth', 'radiotap.vht.nss.0', 'radiotap.mcs.gi', + 'radiotap.vht.gi', 'radiotap.vht.coding.0', 'wlan.ba.bm', + 'wlan.fcs.status', 'wlan.vht.compressed_beamforming_report.snr' + ] + + def __init__(self, config): self.sniffer_proc_pid = None self.log = logger.create_tagged_trace_logger("Tshark Sniffer") self.ssh_config = config["ssh_config"] - self.params = config["sniffer_params"] - self.log_dir = logging_dir - self.type_subtype_dict = { - "0": "Association Requests", - "1": "Association Responses", - "2": "Reassociation Requests", - "3": "Resssociation Responses", - "4": "Probe Requests", - "5": "Probe Responses", - "8": "Beacon", - "9": "ATIM", - "10": "Disassociations", - "11": "Authentications", - "12": "Deauthentications", - "13": "Actions", - "24": "Block ACK Requests", - "25": "Block ACKs", - "26": "PS-Polls", - "27": "RTS", - "28": "CTS", - "29": "ACK", - "30": "CF-Ends", - "31": "CF-Ends/CF-Acks", - "32": "Data", - "33": "Data+CF-Ack", - "34": "Data+CF-Poll", - "35": "Data+CF-Ack+CF-Poll", - "36": "Null", - "37": "CF-Ack", - "38": "CF-Poll", - "39": "CF-Ack+CF-Poll", - "40": "QoS Data", - "41": "QoS Data+CF-Ack", - "42": "QoS Data+CF-Poll", - "43": "QoS Data+CF-Ack+CF-Poll", - "44": "QoS Null", - "46": "QoS CF-Poll (Null)", - "47": "QoS CF-Ack+CF-Poll (Null)" - } - - self.tshark_columns = [ - "frame_number", "frame_time_relative", "mactime", "frame_len", - "rssi", "channel", "ta", "ra", "bssid", "type", "subtype", - "duration", "seq", "retry", "pwrmgmt", "moredata", "ds", "phy", - "radio_datarate", "vht_datarate", "radiotap_mcs_index", "vht_mcs", "wlan_data_rate", - "11n_mcs_index", "11ac_mcs", "11n_bw", "11ac_bw", "vht_nss", "mcs_gi", - "vht_gi", "vht_coding", "ba_bm", "fc_status", - "bf_report" - ] - - - self._tshark_output_columns = [ - "frame_number", - "frame_time_relative", - "mactime", - "ta", - "ra", - "bssid", - "rssi", - "channel", - "frame_len", - "Info", - "radio_datarate", - "radiotap_mcs_index", - "pwrmgmt", - "phy", - "vht_nss", - "vht_mcs", - "vht_datarate", - "11ac_mcs", - "11ac_bw", - "vht_gi", - "vht_coding", - "wlan_data_rate", - "11n_mcs_index", - "11n_bw", - "mcs_gi", - "type", - "subtype", - "duration", - "seq", - "retry", - "moredata", - "ds", - "ba_bm", - "fc_status", - "bf_report" - ] - - - self.tshark_fields = '-T fields -e frame.number -e frame.time_relative -e radiotap.mactime -e frame.len '+\ - '-e radiotap.dbm_antsignal -e wlan_radio.channel '+\ - '-e wlan.ta -e wlan.ra -e wlan.bssid '+\ - '-e wlan.fc.type -e wlan.fc.type_subtype -e wlan.duration -e wlan.seq -e wlan.fc.retry -e wlan.fc.pwrmgt -e wlan.fc.moredata -e wlan.fc.ds '+\ - '-e wlan_radio.phy '+\ - '-e radiotap.datarate -e radiotap.vht.datarate.0 '+\ - '-e radiotap.mcs.index -e radiotap.vht.mcs.0 '+\ - '-e wlan_radio.data_rate -e wlan_radio.11n.mcs_index -e wlan_radio.11ac.mcs '+\ - '-e wlan_radio.11n.bandwidth -e wlan_radio.11ac.bandwidth '+\ - '-e radiotap.vht.nss.0 -e radiotap.mcs.gi -e radiotap.vht.gi -e radiotap.vht.coding.0 '+\ - '-e wlan.ba.bm -e wlan.fcs.status -e wlan.vht.compressed_beamforming_report.snr '+ \ - '-y IEEE802_11_RADIO -E separator="^" ' + self.sniffer_os = config["os"] + self.sniffer_interface = config["interface"] + + #Logging into sniffer + self.log.info("Logging into sniffer.") + self._sniffer_server = ssh.connection.SshConnection( + ssh.settings.from_config(self.ssh_config)) + + self.tshark_fields = self._generate_tshark_fields( + self.TSHARK_FIELDS_LIST) + + self.tshark_path = self._sniffer_server.run("which tshark").stdout @property def _started(self): return self.sniffer_proc_pid is not None - def _ssh_into_sniffer(self): - """logs into the sniffer machine""" - self.log.info("Logging into Sniffer") - self._sniffer_server = ssh.connection.SshConnection( - ssh.settings.from_config(self.ssh_config)) + def _scan_for_networks(self): + """Scans for wireless networks on the sniffer.""" + raise NotImplementedError + + def _init_network_association(self, ssid, password): + """Associates the sniffer to the network to sniff. + + Args: + ssid: SSID of the wireless network to connect to. + password: password of the wireless network to connect to. + """ + raise NotImplementedError + + def _get_tshark_command(self, duration): + """Frames the appropriate tshark command. + + Args: + duration: duration to sniff for. + + Returns: + tshark_command : appropriate tshark command. + """ + + tshark_command = "{} -l -i {} -I -t u -a duration:{}".format( + self.tshark_path, self.sniffer_interface, int(duration)) + + return tshark_command + + def _generate_tshark_fields(self, fields): + """Generates tshark fields to be appended to the tshark command. + + Args: + fields: list of tshark fields to be appended to the tshark command. + + Returns: + tshark_fields: string of tshark fields to be appended to the tshark command. + """ + + tshark_fields = '-T fields -y IEEE802_11_RADIO -E separator="^"' + for field in fields: + tshark_fields = tshark_fields + " -e {}".format(field) + return tshark_fields def _connect_to_network(self, network): - """ Connects to a given network - Connects to a wireless network using networksetup utility + """ Connects to a wireless network using networksetup utility. Args: - network: dictionary of network credentials; SSID and password + network: dictionary of network credentials; SSID and password. """ + self.log.info("Connecting to network {}".format(network["SSID"])) - #Scan to see if the requested SSID is available - scan_result = self._sniffer_server.run("/usr/local/bin/airport -s") + # Scan to see if the requested SSID is available + scan_result = self._scan_for_networks() - if network["SSID"] not in scan_result.stdout: + if network["SSID"] not in scan_result: self.log.error("{} not found in scan".format(network["SSID"])) if "password" not in network.keys(): network["password"] = "" - if set(["SSID", "password"]).issubset(network.keys()): - pass - else: - raise KeyError("Incorrect Network Config") - - connect_command = "networksetup -setairportnetwork en0 {} {}".format( - network["SSID"], network["password"]) - self._sniffer_server.run(connect_command) + self._init_network_association(network["SSID"], network["password"]) def _run_tshark(self, sniffer_command): - """Starts the sniffer""" + """Starts the sniffer. - self.log.info("Starting Sniffer") + Args: + sniffer_command: sniffer command to execute. + """ + + self.log.info("Starting sniffer.") sniffer_job = self._sniffer_server.run_async(sniffer_command) self.sniffer_proc_pid = sniffer_job.stdout def _stop_tshark(self): - """ Stops the sniffer""" + """ Stops the sniffer.""" - self.log.info("Stopping Sniffer") + self.log.info("Stopping sniffer") # while loop to kill the sniffer process + kill_line_logged = False + while True: try: - #Returns error if process was killed already - self._sniffer_server.run("kill -15 {}".format( - str(self.sniffer_proc_pid))) - except: - pass - try: - #Returns 1 if process was killed + # Returns 1 if process was killed self._sniffer_server.run( - "/bin/ps aux| grep {} | grep -v grep".format( + "ps aux| grep {} | grep -v grep".format( self.sniffer_proc_pid)) except: break + try: + # Returns error if process was killed already + if not kill_line_logged: + self.log.info('Killing tshark process.') + kill_line_logged = True + self._sniffer_server.run("kill -15 {}".format( + str(self.sniffer_proc_pid))) + except: + # Except is hit when tshark is already dead but we will break + # out of the loop when confirming process is dead using ps aux + pass + + def _process_tshark_dump(self, temp_dump_file, tag): + """ Process tshark dump for better readability. - def _process_tshark_dump(self, dump, sniffer_tag): - """ Process tshark dump for better readability Processes tshark dump for better readability and saves it to a file. - Adds an info column at the end of each row. - Format of the info columns: subtype of the frame, sequence no and retry status + Adds an info column at the end of each row. Format of the info columns: + subtype of the frame, sequence no and retry status. Args: - dump : string of sniffer capture output - sniffer_tag : tag to be appended to the dump file - + temp_dump_file : string of sniffer capture output. + tag : tag to be appended to the dump file. Returns: - log_file : name of the file where the processed dump is stored + log_file : name of the file where the processed dump is stored. """ - dump = io.StringIO(dump) - log_file = self._get_full_file_path(sniffer_tag) - with open(log_file, "w") as output_csv: - reader = csv.DictReader( - dump, fieldnames=self.tshark_columns, delimiter="^") - writer = csv.DictWriter( - output_csv, fieldnames=self._tshark_output_columns, delimiter="\t") + log_file = self._get_full_file_path(tag) + with open(temp_dump_file, "r") as input_csv, open(log_file, + "w") as output_csv: + reader = csv.DictReader(input_csv, + fieldnames=self.TSHARK_COLUMNS, + delimiter="^") + writer = csv.DictWriter(output_csv, + fieldnames=self.TSHARK_OUTPUT_COLUMNS, + delimiter="\t") writer.writeheader() for row in reader: - if row["subtype"] in self.type_subtype_dict.keys(): + if row["subtype"] in self.TYPE_SUBTYPE_DICT.keys(): row["Info"] = "{sub} S={seq} retry={retry_status}".format( - sub=self.type_subtype_dict[row["subtype"]], + sub=self.TYPE_SUBTYPE_DICT[row["subtype"]], seq=row["seq"], retry_status=row["retry"]) else: - row["Info"] = "{sub} S={seq} retry={retry_status}\n".format( - sub=row["subtype"], - seq=row["seq"], - retry_status=row["retry"]) + row["Info"] = "{} S={} retry={}\n".format( + row["subtype"], row["seq"], row["retry"]) writer.writerow(row) return log_file def start_capture(self, network, duration=30): - """Starts sniffer capture on the specified machine""" + """Starts sniffer capture on the specified machine. + + Args: + network: dict describing network to sniff on. + duration: duration of sniff. + """ # Checking for existing sniffer processes if self._started: self.log.info("Sniffer already running") return - self.tshark_command = "/usr/local/bin/tshark -l -I -t u -a duration:{}".format( - duration) - - # Logging into the sniffer - self._ssh_into_sniffer() - - #Connecting to network + # Connecting to network self._connect_to_network(network) + tshark_command = self._get_tshark_command(duration) + sniffer_command = "{tshark} {fields} > {log_file}".format( - tshark=self.tshark_command, + tshark=tshark_command, fields=self.tshark_fields, log_file=self._get_remote_dump_path()) - #Starting sniffer capture by executing tshark command + # Starting sniffer capture by executing tshark command self._run_tshark(sniffer_command) - def stop_capture(self, sniffer_tag=""): - """Stops the sniffer + def stop_capture(self, tag=""): + """Stops the sniffer. + Args: + tag: tag to be appended to the sniffer output file. Returns: - The name of the processed sniffer dump from the terminated sniffer process + log_file: path to sniffer dump. """ - #Checking if there is an ongoing sniffer capture + # Checking if there is an ongoing sniffer capture if not self._started: self.log.error("No sniffer process running") return # Killing sniffer process self._stop_tshark() - sniffer_dump = self._sniffer_server.run('cat {}'.format( - self._get_remote_dump_path())) - - #Processing writing capture output to file - log_file = self._process_tshark_dump(sniffer_dump.stdout, sniffer_tag) + # Processing writing capture output to file + temp_dump_path = os.path.join(self.log_path, "sniffer_temp_dump.csv") + self._sniffer_server.pull_file(temp_dump_path, + self._get_remote_dump_path()) + log_file = self._process_tshark_dump(temp_dump_path, tag) self.sniffer_proc_pid = None - + utils.exe_cmd("rm -f {}".format(temp_dump_path)) return log_file + + +class TsharkSnifferOnUnix(TsharkSnifferBase): + """Class that implements Tshark based sniffer controller on Unix systems.""" + def _scan_for_networks(self): + """Scans the wireless networks on the sniffer. + + Returns: + scan_results : output of the scan command. + """ + + scan_command = "/usr/local/bin/airport -s" + scan_result = self._sniffer_server.run(scan_command).stdout + + return scan_result + + def _init_network_association(self, ssid, password): + """Associates the sniffer to the network to sniff. + + Associates the sniffer to wireless network to sniff using networksetup utility. + + Args: + ssid: SSID of the wireless network to connect to. + password: password of the wireless network to connect to. + """ + + connect_command = "networksetup -setairportnetwork en0 {} {}".format( + ssid, password) + self._sniffer_server.run(connect_command) + + +class TsharkSnifferOnLinux(TsharkSnifferBase): + """Class that implements Tshark based sniffer controller on Linux systems.""" + def _scan_for_networks(self): + """Scans the wireless networks on the sniffer. + + Returns: + scan_results : output of the scan command. + """ + + scan_command = "nmcli device wifi rescan; nmcli device wifi list" + scan_result = self._sniffer_server.run(scan_command).stdout + + return scan_result + + def _init_network_association(self, ssid, password): + """Associates the sniffer to the network to sniff. + + Associates the sniffer to wireless network to sniff using nmcli utility. + + Args: + ssid: SSID of the wireless network to connect to. + password: password of the wireless network to connect to. + """ + if password != "": + connect_command = "sudo nmcli device wifi connect {} password {}".format( + ssid, password) + else: + connect_command = "sudo nmcli device wifi connect {}".format(ssid) + self._sniffer_server.run(connect_command) diff --git a/acts/framework/acts/test_utils/wifi/wifi_constants.py b/acts/framework/acts/test_utils/wifi/wifi_constants.py index 82da5208a8..21f13d2fc1 100644 --- a/acts/framework/acts/test_utils/wifi/wifi_constants.py +++ b/acts/framework/acts/test_utils/wifi/wifi_constants.py @@ -32,6 +32,24 @@ WIFI_NETWORK_SUGGESTION_POST_CONNECTION = "WifiNetworkSuggestionPostConnection" CONNECT_BY_CONFIG_SUCCESS = 'WifiManagerConnectByConfigOnSuccess' CONNECT_BY_NETID_SUCCESS = 'WifiManagerConnectByNetIdOnSuccess' +# Softap related constants +SOFTAP_CALLBACK_EVENT = "WifiManagerSoftApCallback-" +# Callback Event for softap state change +# WifiManagerSoftApCallback-[callbackId]-OnStateChanged +SOFTAP_STATE_CHANGED = "-OnStateChanged" +# Cllback Event for client number change: +# WifiManagerSoftApCallback-[callbackId]-OnNumClientsChanged +SOFTAP_NUMBER_CLIENTS_CHANGED = "-OnNumClientsChanged" +SOFTAP_NUMBER_CLIENTS_CALLBACK_KEY = "NumClients" +SOFTAP_STATE_CHANGE_CALLBACK_KEY = "State" +WIFI_AP_DISABLING_STATE = 10 +WIFI_AP_DISABLED_STATE = 11 +WIFI_AP_ENABLING_STATE = 12 +WIFI_AP_ENABLED_STATE = 13 +WIFI_AP_FAILED_STATE = 14 +DEFAULT_SOFTAP_TIMEOUT_S = 600 # 10 minutes + + # AP related constants AP_MAIN = "main_AP" AP_AUX = "aux_AP" diff --git a/acts/framework/acts/test_utils/wifi/wifi_power_test_utils.py b/acts/framework/acts/test_utils/wifi/wifi_power_test_utils.py index 34a820b934..ac18a8f3e8 100644 --- a/acts/framework/acts/test_utils/wifi/wifi_power_test_utils.py +++ b/acts/framework/acts/test_utils/wifi/wifi_power_test_utils.py @@ -16,8 +16,8 @@ import logging import time +import os from acts import utils -from acts.controllers import monsoon from acts.libs.proc import job from acts.controllers.ap_lib import bridge_interface as bi from acts.test_utils.wifi import wifi_test_utils as wutils @@ -39,7 +39,7 @@ ENABLED_MODULATED_DTIM = 'gEnableModulatedDTIM=' MAX_MODULATED_DTIM = 'gMaxLIModulatedDTIM=' -def monsoon_data_plot(mon_info, file_path, tag=""): +def monsoon_data_plot(mon_info, monsoon_results, tag=''): """Plot the monsoon current data using bokeh interactive plotting tool. Plotting power measurement data with bokeh to generate interactive plots. @@ -50,9 +50,10 @@ def monsoon_data_plot(mon_info, file_path, tag=""): Args: mon_info: obj with information of monsoon measurement, including - monsoon device object, measurement frequency, duration and - offset etc. - file_path: the path to the monsoon log file with current data + monsoon device object, measurement frequency, duration, etc. + monsoon_results: a MonsoonResult or list of MonsoonResult objects to + to plot. + tag: an extra tag to append to the resulting filename. Returns: plot: the plotting object of bokeh, optional, will be needed if multiple @@ -60,25 +61,35 @@ def monsoon_data_plot(mon_info, file_path, tag=""): dt: the datatable object of bokeh, optional, will be needed if multiple datatables will be combined to one html file. """ + if not isinstance(monsoon_results, list): + monsoon_results = [monsoon_results] + logging.info('Plotting the power measurement data.') + + voltage = monsoon_results[0].voltage + + total_current = 0 + total_samples = 0 + for result in monsoon_results: + total_current += result.average_current * result.num_samples + total_samples += result.num_samples + avg_current = total_current / total_samples + + time_relative = [ + data_point.time + for monsoon_result in monsoon_results + for data_point in monsoon_result.get_data_points() + ] - log = logging.getLogger() - log.info("Plot the power measurement data") - #Get results as monsoon data object from the input file - results = monsoon.MonsoonData.from_text_file(file_path) - #Decouple current and timestamp data from the monsoon object - current_data = [] - timestamps = [] - voltage = results[0].voltage - [current_data.extend(x.data_points) for x in results] - [timestamps.extend(x.timestamps) for x in results] - period = 1 / float(mon_info.freq) - time_relative = [x * period for x in range(len(current_data))] - #Calculate the average current for the test - current_data = [x * 1000 for x in current_data] - avg_current = sum(current_data) / len(current_data) - color = ['navy'] * len(current_data) - - #Preparing the data and source link for bokehn java callback + current_data = [ + data_point.current * 1000 + for monsoon_result in monsoon_results + for data_point in monsoon_result.get_data_points() + ] + + total_data_points = sum(result.num_samples for result in monsoon_results) + color = ['navy'] * total_data_points + + # Preparing the data and source link for bokehn java callback source = ColumnDataSource( data=dict(x0=time_relative, y0=current_data, color=color)) s2 = ColumnDataSource( @@ -88,7 +99,7 @@ def monsoon_data_plot(mon_info, file_path, tag=""): x0=[round(avg_current * voltage, 2)], z1=[round(avg_current * voltage * mon_info.duration, 2)], z2=[round(avg_current * mon_info.duration, 2)])) - #Setting up data table for the output + # Setting up data table for the output columns = [ TableColumn(field='z0', title='Total Duration (s)'), TableColumn(field='y0', title='Average Current (mA)'), @@ -99,26 +110,29 @@ def monsoon_data_plot(mon_info, file_path, tag=""): dt = DataTable( source=s2, columns=columns, width=1300, height=60, editable=True) - plot_title = file_path[file_path.rfind('/') + 1:-4] + tag - output_file("%s/%s.html" % (mon_info.data_path, plot_title)) - TOOLS = ('box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save') + plot_title = (os.path.basename(os.path.splitext(monsoon_results[0].tag)[0]) + + tag) + output_file(os.path.join(mon_info.data_path, plot_title + '.html')) + tools = 'box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save' # Create a new plot with the datatable above plot = figure( - plot_width=1300, plot_height=700, title=plot_title, tools=TOOLS) - plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="width")) - plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="height")) + plot_width=1300, + plot_height=700, + title=plot_title, + tools=tools, + output_backend='webgl') + plot.add_tools(bokeh_tools.WheelZoomTool(dimensions='width')) + plot.add_tools(bokeh_tools.WheelZoomTool(dimensions='height')) plot.line('x0', 'y0', source=source, line_width=2) plot.circle('x0', 'y0', source=source, size=0.5, fill_color='color') plot.xaxis.axis_label = 'Time (s)' plot.yaxis.axis_label = 'Current (mA)' plot.title.text_font_size = {'value': '15pt'} - #Callback Java scripting + # Callback JavaScript source.selected.js_on_change( "indices", - CustomJS( - args=dict(source=source, mytable=dt), - code=""" + CustomJS(args=dict(source=source, mytable=dt), code=""" var inds = cb_obj.indices; var d1 = source.data; var d2 = mytable.source.data; @@ -154,10 +168,9 @@ def monsoon_data_plot(mon_info, file_path, tag=""): mytable.change.emit(); """)) - #Layout the plot and the datatable bar - l = layout([[dt], [plot]]) - save(l) - return [plot, dt] + # Layout the plot and the datatable bar + save(layout([[dt], [plot]])) + return plot, dt def change_dtim(ad, gEnableModulatedDTIM, gMaxLIModulatedDTIM=10): @@ -287,12 +300,12 @@ def bokeh_plot(data_sets, Returns: plot: bokeh plot figure object """ - TOOLS = ('box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save') + tools = 'box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save' plot = figure( plot_width=1300, plot_height=700, title=fig_property['title'], - tools=TOOLS, + tools=tools, output_backend="webgl") plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="width")) plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="height")) @@ -324,7 +337,7 @@ def bokeh_plot(data_sets, legend=str(legend), fill_color=color) - #Plot properties + # Plot properties plot.xaxis.axis_label = fig_property['x_label'] plot.yaxis.axis_label = fig_property['y_label'] plot.legend.location = "top_right" diff --git a/acts/framework/acts/test_utils/wifi/wifi_test_utils.py b/acts/framework/acts/test_utils/wifi/wifi_test_utils.py index ccb6107f25..37426c682b 100755 --- a/acts/framework/acts/test_utils/wifi/wifi_test_utils.py +++ b/acts/framework/acts/test_utils/wifi/wifi_test_utils.py @@ -2012,7 +2012,6 @@ def create_softap_config(): } return config - def start_softap_and_verify(ad, band): """Bring-up softap and verify AP mode and in scan results. @@ -2032,6 +2031,53 @@ def start_softap_and_verify(ad, band): config[WifiEnums.SSID_KEY]) return config +def wait_for_expected_number_of_softap_clients(ad, callbackId, + expected_num_of_softap_clients): + """Wait for the number of softap clients to be updated as expected. + Args: + callbackId: Id of the callback associated with registering. + expected_num_of_softap_clients: expected number of softap clients. + """ + eventStr = wifi_constants.SOFTAP_CALLBACK_EVENT + str( + callbackId) + wifi_constants.SOFTAP_NUMBER_CLIENTS_CHANGED + asserts.assert_equal(ad.ed.pop_event(eventStr, + SHORT_TIMEOUT)['data'][wifi_constants. + SOFTAP_NUMBER_CLIENTS_CALLBACK_KEY], + expected_num_of_softap_clients, + "Number of softap clients doesn't match with expected number") + +def wait_for_expected_softap_state(ad, callbackId, expected_softap_state): + """Wait for the expected softap state change. + Args: + callbackId: Id of the callback associated with registering. + expected_softap_state: The expected softap state. + """ + eventStr = wifi_constants.SOFTAP_CALLBACK_EVENT + str( + callbackId) + wifi_constants.SOFTAP_STATE_CHANGED + asserts.assert_equal(ad.ed.pop_event(eventStr, + SHORT_TIMEOUT)['data'][wifi_constants. + SOFTAP_STATE_CHANGE_CALLBACK_KEY], + expected_softap_state, + "Softap state doesn't match with expected state") + +def get_current_number_of_softap_clients(ad, callbackId): + """pop up all of softap client updated event from queue. + Args: + callbackId: Id of the callback associated with registering. + + Returns: + If exist aleast callback, returns last updated number_of_softap_clients. + Returns None when no any match callback event in queue. + """ + eventStr = wifi_constants.SOFTAP_CALLBACK_EVENT + str( + callbackId) + wifi_constants.SOFTAP_NUMBER_CLIENTS_CHANGED + events = ad.ed.pop_all(eventStr) + for event in events: + num_of_clients = event['data'][wifi_constants. + SOFTAP_NUMBER_CLIENTS_CALLBACK_KEY] + if len(events) == 0: + return None + return num_of_clients def get_ssrdumps(ad, test_name=""): """Pulls dumps in the ssrdump dir @@ -2048,7 +2094,6 @@ def get_ssrdumps(ad, test_name=""): ad.pull_files(logs, log_path) ad.adb.shell("find /data/vendor/ssrdump/ -type f -delete") - def start_pcap(pcap, wifi_band, test_name): """Start packet capture in monitor mode. diff --git a/acts/framework/setup.py b/acts/framework/setup.py index b5e4467bb6..32b8a93cad 100755 --- a/acts/framework/setup.py +++ b/acts/framework/setup.py @@ -39,6 +39,7 @@ install_requires = [ 'xlsxwriter', 'mobly', 'grpcio', + 'Monsoon', # paramiko-ng is needed vs paramiko as currently paramiko does not support # ed25519 ssh keys, which is what Fuchsia uses. 'paramiko-ng', diff --git a/acts/framework/tests/acts_import_unit_test.py b/acts/framework/tests/acts_import_unit_test.py index db9c5e74d6..38dc3bf103 100755 --- a/acts/framework/tests/acts_import_unit_test.py +++ b/acts/framework/tests/acts_import_unit_test.py @@ -48,6 +48,12 @@ else: PY_FILE_REGEX = re.compile('.+\.py$') BLACKLIST = [ + # TODO(markdr): Remove these after BT team evaluates these tests. + 'acts/test_utils/bt/PowerBaseTest.py', + 'tests/google/ble/power/GattPowerTest.py', + 'tests/google/bt/power/A2dpPowerTest.py', + 'tests/google/ble/power/BleScanPowerTest.py', + 'acts/controllers/rohdeschwarz_lib/contest.py', 'acts/controllers/native.py', 'acts/controllers/native_android_device.py', @@ -86,6 +92,7 @@ BLACKLIST = [ 'tests/google/fuchsia/bt/FuchsiaCmdLineTest.py', 'tests/google/fuchsia/bt/gatt/GattServerSetupTest.py', 'tests/google/fuchsia/wlan/RebootStressTest.py', + 'acts/test_utils/gnss/gnss_testlog_utils.py', ] BLACKLIST_DIRECTORIES = [ diff --git a/acts/framework/tests/controllers/monsoon_lib/api/monsoon_test.py b/acts/framework/tests/controllers/monsoon_lib/api/monsoon_test.py index ec274b956e..9d628959d9 100755 --- a/acts/framework/tests/controllers/monsoon_lib/api/monsoon_test.py +++ b/acts/framework/tests/controllers/monsoon_lib/api/monsoon_test.py @@ -209,15 +209,6 @@ class BaseMonsoonTest(unittest.TestCase): 'usbPassthroughMode should not be called when the ' 'state does not change.') - def test_monsoon_usb_auto_sets_usb_state_to_auto(self): - monsoon = MonsoonImpl() - - monsoon.monsoon_usb_auto() - - self.assertEqual(monsoon.status.usbPassthroughMode, - PassthroughStates.AUTO, - 'monsoon_usb_auto() did not disconnect USB.') - def take_samples_always_reestablishes_the_monsoon_connection(self): monsoon = MonsoonImpl() assembly_line = mock.Mock() diff --git a/acts/tests/google/bt/pts/A2dpPtsTest.py b/acts/tests/google/bt/pts/A2dpPtsTest.py index 25ed3ce21b..2c2ffeefa4 100644 --- a/acts/tests/google/bt/pts/A2dpPtsTest.py +++ b/acts/tests/google/bt/pts/A2dpPtsTest.py @@ -29,11 +29,10 @@ class A2dpPtsTest(PtsBaseClass): pts_action_mapping = None def setup_class(self): - super(A2dpPtsTest, self).setup_class() + super().setup_class() self.dut.initialize_bluetooth_controller() # self.dut.set_bluetooth_local_name(self.dut_bluetooth_local_name) local_dut_mac_address = self.dut.get_local_bluetooth_address() - self.pts.set_profile_under_test("A2DP") ics = None ixit = None @@ -56,7 +55,11 @@ class A2dpPtsTest(PtsBaseClass): ics = f_ics_lib.A2DP_ICS ixit = fuchsia_ixit + ### PTS SETUP: Required after ICS, IXIT, and profile is setup ### + self.pts.set_profile_under_test("A2DP") self.pts.set_ics_and_ixit(ics, ixit) + self.pts.setup_pts() + ### End PTS Setup ### self.dut.unbond_all_known_devices() self.dut.start_pairing_helper() diff --git a/acts/tests/google/bt/pts/GattPtsTest.py b/acts/tests/google/bt/pts/GattPtsTest.py index 7883f3727c..4166f8e9b6 100644 --- a/acts/tests/google/bt/pts/GattPtsTest.py +++ b/acts/tests/google/bt/pts/GattPtsTest.py @@ -33,12 +33,11 @@ class GattPtsTest(PtsBaseClass): pts_action_mapping = None def setup_class(self): - super(GattPtsTest, self).setup_class() + super().setup_class() self.dut_bluetooth_local_name = "fs_test" self.dut.initialize_bluetooth_controller() self.dut.set_bluetooth_local_name(self.dut_bluetooth_local_name) local_dut_mac_address = self.dut.get_local_bluetooth_address() - self.pts.set_profile_under_test("GATT") ics = None ixit = None @@ -72,7 +71,11 @@ class GattPtsTest(PtsBaseClass): "Unable to run PTS tests on unsupported hardare {}.".format( type(self.dut))) + ### PTS SETUP: Required after ICS, IXIT, and profile is setup ### + self.pts.set_profile_under_test("GATT") self.pts.set_ics_and_ixit(ics, ixit) + self.pts.setup_pts() + ### End PTS Setup ### self.dut.unbond_all_known_devices() self.dut.start_pairing_helper() diff --git a/acts/tests/google/bt/pts/SdpPtsTest.py b/acts/tests/google/bt/pts/SdpPtsTest.py index ac35cd413c..1dd5d66a24 100644 --- a/acts/tests/google/bt/pts/SdpPtsTest.py +++ b/acts/tests/google/bt/pts/SdpPtsTest.py @@ -99,10 +99,10 @@ PROFILE_ID = int(sig_uuid_constants['AudioSource'], 16) class SdpPtsTest(PtsBaseClass): def setup_class(self): + super().setup_class() self.dut.initialize_bluetooth_controller() # self.dut.set_bluetooth_local_name(self.dut_bluetooth_local_name) local_dut_mac_address = self.dut.get_local_bluetooth_address() - self.pts.set_profile_under_test("SDP") ics = None ixit = None @@ -125,8 +125,12 @@ class SdpPtsTest(PtsBaseClass): ics = f_ics_lib.SDP_ICS ixit = fuchsia_ixit + ### PTS SETUP: Required after ICS, IXIT, and profile is setup ### + self.pts.set_profile_under_test("SDP") self.pts.set_ics_and_ixit(ics, ixit) - super(SdpPtsTest, self).setup_class() + self.pts.setup_pts() + ### End PTS Setup ### + self.dut.unbond_all_known_devices() self.dut.set_discoverable(True) diff --git a/acts/tests/google/coex/hotspot_tests/HotspotWiFiChannelTest.py b/acts/tests/google/coex/hotspot_tests/HotspotWiFiChannelTest.py index b44f784337..c7f0c32922 100644 --- a/acts/tests/google/coex/hotspot_tests/HotspotWiFiChannelTest.py +++ b/acts/tests/google/coex/hotspot_tests/HotspotWiFiChannelTest.py @@ -173,8 +173,11 @@ class HotspotWiFiChannelTest(acts.base_test.BaseTestClass): toggle_airplane_mode(self.log, self.pri_ad, False) self.log.info('Waiting for device to attach.') - self.cmw.wait_for_connected_state() + self.cmw.wait_for_attached_state() self.log.info('Device attached with callbox.') + self.log.debug('Waiting for connected state.') + self.cmw.wait_for_connected_state() + self.log.info('Device connected with callbox') def initiate_wifi_tethering_and_connect(self, wifi_band=None): """Initiates wifi tethering and connects wifi. diff --git a/acts/tests/google/net/DhcpServerTest.py b/acts/tests/google/net/DhcpServerTest.py index c473b25375..6fb6ed4d03 100644 --- a/acts/tests/google/net/DhcpServerTest.py +++ b/acts/tests/google/net/DhcpServerTest.py @@ -1,5 +1,6 @@ from acts import asserts from acts import base_test +from acts import signals from acts.controllers import android_device from acts.test_decorators import test_tracker_info @@ -19,6 +20,9 @@ NETADDR_PREFIX = '192.168.42.' OTHER_NETADDR_PREFIX = '192.168.43.' NETADDR_BROADCAST = '255.255.255.255' SUBNET_BROADCAST = NETADDR_PREFIX + '255' +USB_CHARGE_MODE = 'svc usb setFunctions' +USB_TETHERING_MODE = 'svc usb setFunctions rndis' +DEVICE_IP_ADDRESS = 'ip address' OFFER = 2 @@ -27,14 +31,6 @@ ACK = 5 NAK = 6 -pmc_base_cmd = ( - "am broadcast -a com.android.pmc.action.AUTOPOWER --es PowerAction ") -start_pmc_cmd = ( - "am start -S -n com.android.pmc/com.android.pmc.PMCMainActivity") -pmc_start_usb_tethering_cmd = "%sStartUSBTethering" % pmc_base_cmd -pmc_stop_usb_tethering_cmd = "%sStopUSBTethering" % pmc_base_cmd - - class DhcpServerTest(base_test.BaseTestClass): def setup_class(self): self.dut = self.android_devices[0] @@ -47,8 +43,6 @@ class DhcpServerTest(base_test.BaseTestClass): # Allow using non-67 server ports as long as client uses 68 bind_layers(UDP, BOOTP, dport=CLIENT_PORT) - self.dut.adb.shell(start_pmc_cmd) - self.dut.adb.shell("setprop log.tag.PMC VERBOSE") iflist_before = get_if_list() self._start_usb_tethering(self.dut) self.iface = self._wait_for_new_iface(iflist_before) @@ -98,8 +92,11 @@ class DhcpServerTest(base_test.BaseTestClass): """ self.log.info("Starting USB Tethering") dut.stop_services() - dut.adb.shell(pmc_start_usb_tethering_cmd) - self._wait_for_device(self.dut) + dut.adb.shell(USB_TETHERING_MODE, ignore_status=True) + dut.adb.wait_for_device() + dut.start_services() + if 'rndis' not in dut.adb.shell(DEVICE_IP_ADDRESS): + raise signals.TestFailure('Unable to enable USB tethering.') self.USB_TETHERED = True def _stop_usb_tethering(self, dut): @@ -109,8 +106,9 @@ class DhcpServerTest(base_test.BaseTestClass): 1. dut - ad object """ self.log.info("Stopping USB Tethering") - dut.adb.shell(pmc_stop_usb_tethering_cmd) - self._wait_for_device(self.dut) + dut.stop_services() + dut.adb.shell(USB_CHARGE_MODE) + dut.adb.wait_for_device() dut.start_services() self.USB_TETHERED = False diff --git a/acts/tests/google/net/DnsOverTlsTest.py b/acts/tests/google/net/DnsOverTlsTest.py index e8a883d635..84646d7f9e 100644 --- a/acts/tests/google/net/DnsOverTlsTest.py +++ b/acts/tests/google/net/DnsOverTlsTest.py @@ -95,7 +95,7 @@ class DnsOverTlsTest(base_test.BaseTestClass): """ try: packets = rdpcap(pcap_file) - except Scapy_Excaption: + except Scapy_Exception: asserts.fail("Not a valid pcap file") for pkt in packets: summary = "%s" % pkt.summary() diff --git a/acts/tests/google/power/PowerBaselineTest.py b/acts/tests/google/power/PowerBaselineTest.py index b75514423d..d8ef277b2e 100644 --- a/acts/tests/google/power/PowerBaselineTest.py +++ b/acts/tests/google/power/PowerBaselineTest.py @@ -31,7 +31,7 @@ class PowerBaselineTest(PowerBaseTest): self.dut.droid.goToSleepNow() # Measure power - self.collect_power_data() + result = self.collect_power_data() # Check if power measurement is below the required value - self.pass_fail_check() + self.pass_fail_check(result.average_current) diff --git a/acts/tests/google/power/bt/PowerBLEadvertiseTest.py b/acts/tests/google/power/bt/PowerBLEadvertiseTest.py new file mode 100644 index 0000000000..c12d2293e1 --- /dev/null +++ b/acts/tests/google/power/bt/PowerBLEadvertiseTest.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3.4 +# +# Copyright 2018 - The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import acts.test_utils.bt.BleEnum as bleenum +import acts.test_utils.bt.bt_power_test_utils as btputils +import acts.test_utils.power.PowerBTBaseTest as PBtBT + +BLE_LOCATION_SCAN_ENABLE = 'settings put secure location_mode 3' +EXTRA_ADV_TIME = 10 + + +class PowerBLEadvertiseTest(PBtBT.PowerBTBaseTest): + def __init__(self, configs): + super().__init__(configs) + req_params = ['adv_modes', 'adv_power_levels', 'adv_duration'] + self.unpack_userparams(req_params) + # Loop all advertise modes and power levels + for adv_mode in self.adv_modes: + for adv_power_level in self.adv_power_levels: + self.generate_test_case(adv_mode, adv_power_level, + self.adv_duration) + + def setup_class(self): + + super().setup_class() + self.dut.adb.shell(BLE_LOCATION_SCAN_ENABLE) + # Make sure during power measurement, advertisement is always on + self.mon_info.duration = ( + self.adv_duration - self.mon_offset - EXTRA_ADV_TIME) + + def generate_test_case(self, adv_mode, adv_power_level, adv_duration): + def test_case_fn(): + + self.measure_ble_advertise_power(adv_mode, adv_power_level, + adv_duration) + + adv_mode_str = bleenum.AdvertiseSettingsAdvertiseMode(adv_mode).name + adv_txpl_str = bleenum.AdvertiseSettingsAdvertiseTxPower( + adv_power_level).name.strip('ADVERTISE').strip('_') + test_case_name = ('test_BLE_{}_{}'.format(adv_mode_str, adv_txpl_str)) + setattr(self, test_case_name, test_case_fn) + + def measure_ble_advertise_power(self, adv_mode, adv_power_level, + adv_duration): + + btputils.start_apk_ble_adv(self.dut, adv_mode, adv_power_level, + adv_duration) + time.sleep(EXTRA_ADV_TIME) + self.measure_power_and_validate() diff --git a/acts/tests/google/power/bt/PowerBLEscanTest.py b/acts/tests/google/power/bt/PowerBLEscanTest.py new file mode 100644 index 0000000000..8ed77b5a94 --- /dev/null +++ b/acts/tests/google/power/bt/PowerBLEscanTest.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3.4 +# +# Copyright 2018 - The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import acts.test_utils.bt.BleEnum as bleenum +import acts.test_utils.bt.bt_power_test_utils as btputils +import acts.test_utils.power.PowerBTBaseTest as PBtBT + +BLE_LOCATION_SCAN_ENABLE = 'settings put secure location_mode 3' +EXTRA_SCAN_TIME = 10 + + +class PowerBLEscanTest(PBtBT.PowerBTBaseTest): + def __init__(self, configs): + super().__init__(configs) + req_params = ['scan_modes', 'scan_duration'] + self.unpack_userparams(req_params) + + for scan_mode in self.scan_modes: + self.generate_test_case_no_devices_around(scan_mode, + self.scan_duration) + + def setup_class(self): + + super().setup_class() + self.dut.adb.shell(BLE_LOCATION_SCAN_ENABLE) + # Make sure during power measurement, scan is always on + self.mon_info.duration = ( + self.scan_duration - self.mon_offset - EXTRA_SCAN_TIME) + + def generate_test_case_no_devices_around(self, scan_mode, scan_duration): + def test_case_fn(): + + self.measure_ble_scan_power(scan_mode, scan_duration) + + test_case_name = ('test_BLE_{}_no_advertisers'.format( + bleenum.ScanSettingsScanMode(scan_mode).name)) + setattr(self, test_case_name, test_case_fn) + + def measure_ble_scan_power(self, scan_mode, scan_duration): + + btputils.start_apk_ble_scan(self.dut, scan_mode, scan_duration) + time.sleep(EXTRA_SCAN_TIME) + self.measure_power_and_validate() diff --git a/acts/tests/google/power/bt/PowerBTa2dpTest.py b/acts/tests/google/power/bt/PowerBTa2dpTest.py new file mode 100644 index 0000000000..8122fc0748 --- /dev/null +++ b/acts/tests/google/power/bt/PowerBTa2dpTest.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3.4 +# +# Copyright 2018 - The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import acts.test_utils.bt.bt_test_utils as btutils +import acts.test_utils.power.PowerBTBaseTest as PBtBT +from acts import asserts +from acts.test_utils.bt import BtEnum + +EXTRA_PLAY_TIME = 10 + + +class PowerBTa2dpTest(PBtBT.PowerBTBaseTest): + def __init__(self, configs): + super().__init__(configs) + req_params = ['codecs', 'tx_power_levels', 'atten_pl_settings'] + self.unpack_userparams(req_params) + # Loop all codecs and tx power levels + for codec_config in self.codecs: + for tpl in self.tx_power_levels: + self.generate_test_case(codec_config, tpl) + + def setup_test(self): + super().setup_test() + btutils.connect_phone_to_headset(self.dut, self.bt_device, 60) + vol = self.dut.droid.getMaxMediaVolume() * self.volume + self.dut.droid.setMediaVolume(0) + time.sleep(1) + self.dut.droid.setMediaVolume(int(vol)) + + + def generate_test_case(self, codec_config, tpl): + def test_case_fn(): + self.measure_a2dp_power(codec_config, tpl) + + test_case_name = ('test_BTa2dp_{}_codec_at_PL{}'.format( + codec_config['codec_type'], tpl)) + setattr(self, test_case_name, test_case_fn) + + def measure_a2dp_power(self, codec_config, tpl): + + current_codec = self.dut.droid.bluetoothA2dpGetCurrentCodecConfig() + current_codec_type = BtEnum.BluetoothA2dpCodecType( + current_codec['codecType']).name + if current_codec_type != codec_config['codec_type']: + codec_set = btutils.set_bluetooth_codec(self.dut, **codec_config) + asserts.assert_true(codec_set, 'Codec configuration failed.') + else: + self.log.info('Current Codec is {}, no need to change'.format( + current_codec_type)) + + # Set attenuation so BT tx at desired power level + tpl = 'PL' + str(tpl) + self.set_attenuation(self.atten_pl_settings[tpl]) + self.log.info('Setting Attenuator to {} dB'.format(self.atten_pl_settings[tpl])) + + self.media.play() + self.log.info('Running A2DP with codec {} at {}'.format( + codec_config['codec_type'], tpl)) + self.dut.droid.goToSleepNow() + time.sleep(EXTRA_PLAY_TIME) + self.measure_power_and_validate()
\ No newline at end of file diff --git a/acts/tests/google/power/bt/PowerBTbaselineTest.py b/acts/tests/google/power/bt/PowerBTbaselineTest.py deleted file mode 100644 index f4050c8de5..0000000000 --- a/acts/tests/google/power/bt/PowerBTbaselineTest.py +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3.4 -# -# Copyright 2018 - The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from acts.test_decorators import test_tracker_info -import acts.test_utils.power.PowerBTBaseTest as PBtBT - - -class PowerBTbaselineTest(PBtBT.PowerBTBaseTest): - def bt_baseline_test_func(self): - """Base function for BT baseline measurement. - - Steps: - 1. Sets the phone in airplane mode, disables gestures and location - 2. Turns ON/OFF BT, BLE and screen according to test conditions - 3. Measures the power consumption - 4. Asserts pass/fail criteria based on measured power - """ - - # Decode the test params from test name - attrs = ['screen_status', 'bt_status', 'ble_status', 'scan_status'] - indices = [2, 4, 6, 7] - self.decode_test_configs(attrs, indices) - # Setup the phoen at desired state - self.phone_setup_for_BT(self.test_configs.bt_status, - self.test_configs.ble_status, - self.test_configs.screen_status) - if self.test_configs.scan_status == 'connectable': - self.dut.droid.bluetoothMakeConnectable() - elif self.test_configs.scan_status == 'discoverable': - self.dut.droid.bluetoothMakeDiscoverable( - self.mon_info.duration + self.mon_info.offset) - self.measure_power_and_validate() - - # Test cases- Baseline - @test_tracker_info(uuid='3f8ac0cb-f20d-4569-a58e-6009c89ea049') - def test_screen_OFF_bt_ON_ble_ON_connectable(self): - self.bt_baseline_test_func() - - @test_tracker_info(uuid='d54a992e-37ed-460a-ada7-2c51941557fd') - def test_screen_OFF_bt_ON_ble_ON_discoverable(self): - self.bt_baseline_test_func() - - @test_tracker_info(uuid='8f4c36b5-b18e-4aa5-9fe5-aafb729c1034') - def test_screen_ON_bt_ON_ble_ON_connectable(self): - self.bt_baseline_test_func() - - @test_tracker_info(uuid='7128356f-67d8-46b3-9d6b-1a4c9a7a1745') - def test_screen_ON_bt_ON_ble_ON_discoverable(self): - self.bt_baseline_test_func() diff --git a/acts/tests/google/power/bt/PowerBTcalibrationTest.py b/acts/tests/google/power/bt/PowerBTcalibrationTest.py new file mode 100644 index 0000000000..dffcc67231 --- /dev/null +++ b/acts/tests/google/power/bt/PowerBTcalibrationTest.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3.4 +# +# Copyright 2018 - The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import csv +import os +import time +import acts.test_utils.bt.bt_test_utils as btutils +import acts.test_utils.power.PowerBTBaseTest as PBtBT +from acts import utils + +EXTRA_PLAY_TIME = 10 + + +class PowerBTcalibrationTest(PBtBT.PowerBTBaseTest): + def setup_test(self): + + super().setup_test() + self.attenuator = self.attenuators[0] + btutils.enable_bqr(self.dut) + btutils.enable_bluetooth(self.dut.droid, self.dut.ed) + btutils.connect_phone_to_headset(self.dut, self.bt_device, 60) + vol = self.dut.droid.getMaxMediaVolume() * self.volume + self.dut.droid.setMediaVolume(int(vol)) + + self.cal_data_path = os.path.join(self.log_path, 'Calibration') + self.log_file = os.path.join(self.cal_data_path, 'Cal_data.csv') + utils.create_dir(os.path.dirname(self.log_file)) + + + def test_calibrate(self): + """Run calibration to get attenuation value at each power level + + """ + + self.cal_matrix = [] + self.media.play() + time.sleep(EXTRA_PLAY_TIME) + + # Loop through attenuation in 1 dB step until reaching at PL10 + for i in range(int(self.attenuator.get_max_atten())): + + self.attenuator.set_atten(i) + bt_metrics_dict = btutils.get_bt_metric(self.dut) + pwl = int(bt_metrics_dict['pwlv'][self.dut.serial]) + self.cal_matrix.append([i, pwl]) + if pwl == 10: + break + + # Write cal results to csv + with open(self.log_file, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerows(self.cal_matrix) diff --git a/acts/tests/google/power/bt/PowerBTidleTest.py b/acts/tests/google/power/bt/PowerBTidleTest.py new file mode 100644 index 0000000000..bab79d0836 --- /dev/null +++ b/acts/tests/google/power/bt/PowerBTidleTest.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3.4 +# +# Copyright 2018 - The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time +import acts.test_utils.power.PowerBTBaseTest as PBtBT +import acts.test_utils.bt.bt_test_utils as btutils + +SCREEN_OFF_WAIT_TIME = 2 + + +class PowerBTidleTest(PBtBT.PowerBTBaseTest): + def setup_class(self): + + super().setup_class() + btutils.enable_bluetooth(self.dut.droid, self.dut.ed) + + # Test cases- Baseline + def test_bt_on_unconnected_connectable(self): + """BT turned on connectable mode. + + Page scan only. + """ + self.dut.droid.bluetoothMakeConnectable() + self.dut.droid.goToSleepNow() + time.sleep(SCREEN_OFF_WAIT_TIME) + self.measure_power_and_validate() + + def test_bt_on_unconnected_discoverable(self): + """BT turned on discoverable mode. + + Page and inquiry scan. + """ + self.dut.droid.bluetoothMakeConnectable() + self.dut.droid.bluetoothMakeDiscoverable() + self.dut.droid.goToSleepNow() + time.sleep(SCREEN_OFF_WAIT_TIME) + self.measure_power_and_validate() + + def test_bt_connected_idle(self): + """BT idle after connecting to headset. + + """ + btutils.connect_phone_to_headset(self.dut, self.bt_device, 60) + self.dut.droid.goToSleepNow() + time.sleep(SCREEN_OFF_WAIT_TIME) + self.measure_power_and_validate() diff --git a/acts/tests/google/power/bt/PowerBTscanTest.py b/acts/tests/google/power/bt/PowerBTscanTest.py deleted file mode 100644 index 0ef622c648..0000000000 --- a/acts/tests/google/power/bt/PowerBTscanTest.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python3.4 -# -# Copyright 2018 - The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import acts.test_utils.power.PowerBTBaseTest as PBtBT -from acts.test_decorators import test_tracker_info - - -class PowerBTscanTest(PBtBT.PowerBTBaseTest): - def ble_scan_base_func(self): - """Base function to start a generic BLE scan and measures the power - - Steps: - 1. Sets the phone in airplane mode, disables gestures and location - 2. Turns ON/OFF BT, BLE and screen according to test conditions - 3. Sends the adb shell command to PMC to start scan - 4. Measures the power consumption - 5. Asserts pass/fail criteria based on measured power - """ - # Decode the test params from test name - attrs = ['screen_status', 'bt_status', 'ble_status', 'scan_mode'] - indices = [2, 4, 6, -1] - self.decode_test_configs(attrs, indices) - if self.test_configs.scan_mode == 'lowpower': - scan_mode = 'low_power' - elif self.test_configs.scan_mode == 'lowlatency': - scan_mode = 'low_latency' - else: - scan_mode = self.test_configs.scan_mode - self.phone_setup_for_BT(self.test_configs.bt_status, - self.test_configs.ble_status, - self.test_configs.screen_status) - self.start_pmc_ble_scan(scan_mode, self.mon_info.offset, - self.mon_info.duration) - self.measure_power_and_validate() - - # Test Cases: BLE Scans + Filtered scans - @test_tracker_info(uuid='e9a36161-1d0c-4b9a-8bd8-80fef8cdfe28') - def test_screen_ON_bt_ON_ble_ON_default_scan_balanced(self): - self.ble_scan_base_func() - - @test_tracker_info(uuid='5fa61bf4-5f04-40bf-af52-6644b534d02e') - def test_screen_OFF_bt_ON_ble_ON_filter_scan_opportunistic(self): - self.ble_scan_base_func() - - @test_tracker_info(uuid='512b6cde-be83-43b0-b799-761380ba69ff') - def test_screen_OFF_bt_ON_ble_ON_filter_scan_lowpower(self): - self.ble_scan_base_func() - - @test_tracker_info(uuid='3a526838-ae7b-4cdb-bc29-89a5503d2306') - def test_screen_OFF_bt_ON_ble_ON_filter_scan_balanced(self): - self.ble_scan_base_func() - - @test_tracker_info(uuid='03a57cfd-4269-4a09-8544-84f878d2e801') - def test_screen_OFF_bt_ON_ble_ON_filter_scan_lowlatency(self): - self.ble_scan_base_func() - - # Test Cases: Background scans - @test_tracker_info(uuid='20145317-e362-4bfd-9860-4ceddf764784') - def test_screen_ON_bt_OFF_ble_ON_background_scan_lowlatency(self): - self.ble_scan_base_func() - - @test_tracker_info(uuid='00a53dc3-2c33-43c4-b356-dba93249b823') - def test_screen_ON_bt_OFF_ble_ON_background_scan_lowpower(self): - self.ble_scan_base_func() - - @test_tracker_info(uuid='b7185d64-631f-4b18-8d0b-4e14b80db375') - def test_screen_OFF_bt_OFF_ble_ON_background_scan_lowlatency(self): - self.ble_scan_base_func() - - @test_tracker_info(uuid='93eb05da-a577-409c-8208-6af1899a10c2') - def test_screen_OFF_bt_OFF_ble_ON_background_scan_lowpower(self): - self.ble_scan_base_func() diff --git a/acts/tests/google/power/gnss/PowerGnssDpoSimTest.py b/acts/tests/google/power/gnss/PowerGnssDpoSimTest.py index 09b47a62aa..9c2ce9a2ef 100644 --- a/acts/tests/google/power/gnss/PowerGnssDpoSimTest.py +++ b/acts/tests/google/power/gnss/PowerGnssDpoSimTest.py @@ -16,10 +16,12 @@ import acts.test_utils.power.PowerGnssBaseTest as GBT from acts.test_utils.gnss import dut_log_test_utils as diaglog +from acts.test_utils.gnss import gnss_test_utils as gutil import time import os from acts import utils -MDLOG_RUNNING_TIME = 120 +MDLOG_RUNNING_TIME = 300 +DUT_ACTION_WAIT_TIME = 2 class PowerGnssDpoSimTest(GBT.PowerGnssBaseTest): """Power baseline tests for rockbottom state. @@ -33,27 +35,35 @@ class PowerGnssDpoSimTest(GBT.PowerGnssBaseTest): Decode the test config from the test name, set device to desired state. Measure power and plot results. """ - self.collect_power_data() - self.pass_fail_check() + result = self.collect_power_data() + self.pass_fail_check(result.average_current) # Test cases def test_gnss_dpoOFF_measurement(self): utils.set_location_service(self.dut, True) + time.sleep(DUT_ACTION_WAIT_TIME) + gutil.start_gnss_by_gtw_gpstool(self.dut, state=True, type="gnss", bgdisplay=True) self.dut.send_keycode("SLEEP") + time.sleep(DUT_ACTION_WAIT_TIME) self.measure_gnsspower_test_func() diaglog.start_diagmdlog_background(self.dut, maskfile=self.maskfile) self.disconnect_usb(self.dut, MDLOG_RUNNING_TIME) qxdm_log_path = os.path.join(self.log_path, 'QXDM') diaglog.stop_background_diagmdlog(self.dut, qxdm_log_path) + gutil.start_gnss_by_gtw_gpstool(self.dut, state=False) def test_gnss_dpoON_measurement(self): utils.set_location_service(self.dut, True) + time.sleep(DUT_ACTION_WAIT_TIME) + gutil.start_gnss_by_gtw_gpstool(self.dut, state=True, type="gnss", bgdisplay=True) self.dut.send_keycode("SLEEP") + time.sleep(DUT_ACTION_WAIT_TIME) self.measure_gnsspower_test_func() diaglog.start_diagmdlog_background(self.dut, maskfile=self.maskfile) self.disconnect_usb(self.dut, MDLOG_RUNNING_TIME) qxdm_log_path = os.path.join(self.log_path, 'QXDM') diaglog.stop_background_diagmdlog(self.dut, qxdm_log_path) + gutil.start_gnss_by_gtw_gpstool(self.dut, state=False) def test_gnss_rockbottom(self): self.dut.send_keycode("SLEEP") diff --git a/acts/tests/google/power/tel/lab/PowerTelHotspotTest.py b/acts/tests/google/power/tel/lab/PowerTelHotspotTest.py index 66aa45815c..e5aabb76f9 100644 --- a/acts/tests/google/power/tel/lab/PowerTelHotspotTest.py +++ b/acts/tests/google/power/tel/lab/PowerTelHotspotTest.py @@ -119,7 +119,7 @@ class PowerTelHotspotTest(PowerTelTrafficTest): iperf_helpers = self.start_tel_traffic(self.android_devices[1]) # Measure power - self.collect_power_data() + result = self.collect_power_data() # Wait for iPerf to finish time.sleep(self.IPERF_MARGIN + 2) @@ -129,7 +129,7 @@ class PowerTelHotspotTest(PowerTelTrafficTest): iperf_helpers) # Checks if power is below the required threshold. - self.pass_fail_check() + self.pass_fail_check(result.average_current) def setup_test(self): """ Executed before every test case. diff --git a/acts/tests/google/power/tel/lab/PowerTelIdleTest.py b/acts/tests/google/power/tel/lab/PowerTelIdleTest.py index 934fe20dc0..f1197988c6 100644 --- a/acts/tests/google/power/tel/lab/PowerTelIdleTest.py +++ b/acts/tests/google/power/tel/lab/PowerTelIdleTest.py @@ -33,7 +33,7 @@ class PowerTelIdleTest(PWCEL.PowerCellularLabBaseTest): self.cellular_simulator.wait_until_idle_state(idle_wait_time) # Measure power - self.collect_power_data() + result = self.collect_power_data() # Check if power measurement is below the required value - self.pass_fail_check() + self.pass_fail_check(result.average_current) diff --git a/acts/tests/google/power/tel/lab/PowerTelTrafficTest.py b/acts/tests/google/power/tel/lab/PowerTelTrafficTest.py index 97094e3017..b373fdb3d0 100644 --- a/acts/tests/google/power/tel/lab/PowerTelTrafficTest.py +++ b/acts/tests/google/power/tel/lab/PowerTelTrafficTest.py @@ -55,11 +55,6 @@ class PowerTelTrafficTest(PWCEL.PowerCellularLabBaseTest): super().__init__(controllers) - # Verify that at least one PacketSender controller has been initialized - if not hasattr(self, 'packet_senders'): - raise RuntimeError('At least one packet sender controller needs ' - 'to be defined in the test config files.') - # These variables are passed to iPerf when starting data # traffic with the -b parameter to limit throughput on # the application layer. @@ -76,6 +71,14 @@ class PowerTelTrafficTest(PWCEL.PowerCellularLabBaseTest): self.ul_tput_logger = BlackboxMetricLogger.for_test_case( metric_name='avg_ul_tput') + def setup_class(self): + super().setup_class() + + # Verify that at least one PacketSender controller has been initialized + if not hasattr(self, 'packet_senders'): + raise RuntimeError('At least one packet sender controller needs ' + 'to be defined in the test config files.') + def setup_test(self): """ Executed before every test case. @@ -155,7 +158,7 @@ class PowerTelTrafficTest(PWCEL.PowerCellularLabBaseTest): iperf_helpers = self.start_tel_traffic(self.dut) # Measure power - self.collect_power_data() + result = self.collect_power_data() # Wait for iPerf to finish time.sleep(self.IPERF_MARGIN + 2) @@ -164,9 +167,9 @@ class PowerTelTrafficTest(PWCEL.PowerCellularLabBaseTest): self.iperf_results = self.get_iperf_results(self.dut, iperf_helpers) # Check if power measurement is below the required value - self.pass_fail_check() + self.pass_fail_check(result.average_current) - return self.test_result, self.iperf_results + return result.average_current, self.iperf_results def get_iperf_results(self, device, iperf_helpers): """ Pulls iperf results from the device. @@ -193,7 +196,7 @@ class PowerTelTrafficTest(PWCEL.PowerCellularLabBaseTest): return throughput - def pass_fail_check(self): + def pass_fail_check(self, average_current=None): """ Checks power consumption and throughput. Uses the base class method to check power consumption. Also, compares @@ -227,7 +230,7 @@ class PowerTelTrafficTest(PWCEL.PowerCellularLabBaseTest): direction, round(throughput, 3), round(expected_t, 3), round(throughput / expected_t, 3))) - super().pass_fail_check() + super().pass_fail_check(average_current) def start_tel_traffic(self, client_host): """ Starts iPerf in the indicated device and initiates traffic. @@ -241,7 +244,6 @@ class PowerTelTrafficTest(PWCEL.PowerCellularLabBaseTest): Returns: A list of iperf helpers. """ - # The iPerf server is hosted in this computer self.iperf_server_address = scapy.get_if_addr( self.packet_senders[0].interface) diff --git a/acts/tests/google/power/tel/lab/PowerTelVoiceCallTest.py b/acts/tests/google/power/tel/lab/PowerTelVoiceCallTest.py index f0d0eda6ce..93b69c6e2b 100644 --- a/acts/tests/google/power/tel/lab/PowerTelVoiceCallTest.py +++ b/acts/tests/google/power/tel/lab/PowerTelVoiceCallTest.py @@ -70,10 +70,10 @@ class PowerTelVoiceCallTest(PWCEL.PowerCellularLabBaseTest): self.dut.droid.goToSleepNow() # Measure power - self.collect_power_data() + result = self.collect_power_data() # End the call hangup_call(self.log, self.dut) # Check if power measurement is within the required values - self.pass_fail_check() + self.pass_fail_check(result.average_current) diff --git a/acts/tests/google/power/wifi/PowerWiFiHotspotTest.py b/acts/tests/google/power/wifi/PowerWiFiHotspotTest.py index f554d12bde..57e926376a 100644 --- a/acts/tests/google/power/wifi/PowerWiFiHotspotTest.py +++ b/acts/tests/google/power/wifi/PowerWiFiHotspotTest.py @@ -158,7 +158,7 @@ class PowerWiFiHotspotTest(PWBT.PowerWiFiBaseTest): time.sleep(2) # Measure power - self.collect_power_data() + result = self.collect_power_data() if traffic: # Wait for iperf to finish @@ -168,7 +168,7 @@ class PowerWiFiHotspotTest(PWBT.PowerWiFiBaseTest): self.client_iperf_helper.process_iperf_results( self.dut, self.log, self.iperf_servers, self.test_name) - self.pass_fail_check() + self.pass_fail_check(result.average_current) def power_idle_tethering_test(self): """ Start power test when Hotspot is idle diff --git a/acts/tests/google/power/wifi/PowerWiFimulticastTest.py b/acts/tests/google/power/wifi/PowerWiFimulticastTest.py index 5a6108cdfb..8832469980 100644 --- a/acts/tests/google/power/wifi/PowerWiFimulticastTest.py +++ b/acts/tests/google/power/wifi/PowerWiFimulticastTest.py @@ -27,6 +27,22 @@ DNS_SHORT_LIFETIME = 3 class PowerWiFimulticastTest(PWBT.PowerWiFiBaseTest): + def setup_class(self): + super().setup_class() + self.unpack_userparams(sub_mask="255.255.255.0", + mac_dst="get_from_dut", + mac_src="get_local", + ipv4_dst="get_from_dut", + ipv4_src="get_local", + ipv6_dst="get_from_dut", + ipv6_src="get_local", + ipv6_src_type="LINK_LOCAL", + ipv4_gwt="192.168.1.1", + mac_dst_fake="40:90:28:EF:4B:20", + ipv4_dst_fake="192.168.1.60", + ipv6_dst_fake="fe80::300f:40ee:ee0a:5000", + interval=1) + def set_connection(self): """Setup connection between AP and client. @@ -38,8 +54,9 @@ class PowerWiFimulticastTest(PWBT.PowerWiFiBaseTest): indices = [2, 4] self.decode_test_configs(attrs, indices) # Change DTIMx1 on the phone to receive all Multicast packets - rebooted = wputils.change_dtim( - self.dut, gEnableModulatedDTIM=1, gMaxLIModulatedDTIM=10) + rebooted = wputils.change_dtim(self.dut, + gEnableModulatedDTIM=1, + gMaxLIModulatedDTIM=10) self.dut.log.info('DTIM value of the phone is now DTIMx1') if rebooted: self.dut_rockbottom() @@ -87,8 +104,8 @@ class PowerWiFimulticastTest(PWBT.PowerWiFiBaseTest): self.set_connection() self.pkt_gen_config = wputils.create_pkt_config(self) pkt_gen = pkt_utils.ArpGenerator(**self.pkt_gen_config) - packet = pkt_gen.generate( - ip_dst='0.0.0.0', eth_dst=self.pkt_gen_config['dst_mac']) + packet = pkt_gen.generate(ip_dst='0.0.0.0', + eth_dst=self.pkt_gen_config['dst_mac']) self.sendPacketAndMeasure(packet) @test_tracker_info(uuid='5dcb16f1-725c-45de-8103-340104d60a22') @@ -96,8 +113,8 @@ class PowerWiFimulticastTest(PWBT.PowerWiFiBaseTest): self.set_connection() self.pkt_gen_config = wputils.create_pkt_config(self) pkt_gen = pkt_utils.ArpGenerator(**self.pkt_gen_config) - packet = pkt_gen.generate( - ip_dst=self.ipv4_dst_fake, eth_dst=self.pkt_gen_config['dst_mac']) + packet = pkt_gen.generate(ip_dst=self.ipv4_dst_fake, + eth_dst=self.pkt_gen_config['dst_mac']) self.sendPacketAndMeasure(packet) @test_tracker_info(uuid='5ec4800f-a82e-4462-8b65-4fcd0b1940a2') @@ -105,12 +122,11 @@ class PowerWiFimulticastTest(PWBT.PowerWiFiBaseTest): self.set_connection() self.pkt_gen_config = wputils.create_pkt_config(self) pkt_gen = pkt_utils.ArpGenerator(**self.pkt_gen_config) - packet = pkt_gen.generate( - op='is-at', - ip_src='0.0.0.0', - ip_dst=self.ipv4_dst_fake, - hwdst=self.mac_dst_fake, - eth_dst=self.pkt_gen_config['dst_mac']) + packet = pkt_gen.generate(op='is-at', + ip_src='0.0.0.0', + ip_dst=self.ipv4_dst_fake, + hwdst=self.mac_dst_fake, + eth_dst=self.pkt_gen_config['dst_mac']) self.sendPacketAndMeasure(packet) @test_tracker_info(uuid='6c5c0e9e-7a00-43d0-a6e8-355141467703') @@ -118,11 +134,10 @@ class PowerWiFimulticastTest(PWBT.PowerWiFiBaseTest): self.set_connection() self.pkt_gen_config = wputils.create_pkt_config(self) pkt_gen = pkt_utils.ArpGenerator(**self.pkt_gen_config) - packet = pkt_gen.generate( - op='is-at', - ip_dst=self.ipv4_dst_fake, - hwdst=self.mac_dst_fake, - eth_dst=self.pkt_gen_config['dst_mac']) + packet = pkt_gen.generate(op='is-at', + ip_dst=self.ipv4_dst_fake, + hwdst=self.mac_dst_fake, + eth_dst=self.pkt_gen_config['dst_mac']) self.sendPacketAndMeasure(packet) @test_tracker_info(uuid='8e534d3b-5a25-429a-a1bb-8119d7d28b5a') @@ -178,8 +193,9 @@ class PowerWiFimulticastTest(PWBT.PowerWiFiBaseTest): self.set_connection() self.pkt_gen_config = wputils.create_pkt_config(self) pkt_gen = pkt_utils.RaGenerator(**self.pkt_gen_config) - packet = pkt_gen.generate( - RA_LONG_LIFETIME, enableDNS=True, dns_lifetime=DNS_SHORT_LIFETIME) + packet = pkt_gen.generate(RA_LONG_LIFETIME, + enableDNS=True, + dns_lifetime=DNS_SHORT_LIFETIME) self.sendPacketAndMeasure(packet) @test_tracker_info(uuid='84d2f1ff-bd4f-46c6-9b06-826d9b14909c') @@ -187,8 +203,9 @@ class PowerWiFimulticastTest(PWBT.PowerWiFiBaseTest): self.set_connection() self.pkt_gen_config = wputils.create_pkt_config(self) pkt_gen = pkt_utils.RaGenerator(**self.pkt_gen_config) - packet = pkt_gen.generate( - RA_LONG_LIFETIME, enableDNS=True, dns_lifetime=DNS_LONG_LIFETIME) + packet = pkt_gen.generate(RA_LONG_LIFETIME, + enableDNS=True, + dns_lifetime=DNS_LONG_LIFETIME) self.sendPacketAndMeasure(packet) @test_tracker_info(uuid='4a17e74f-3e7f-4e90-ac9e-884a7c13cede') @@ -333,8 +350,9 @@ class PowerWiFimulticastTest(PWBT.PowerWiFiBaseTest): self.set_connection() self.pkt_gen_config = wputils.create_pkt_config(self) pkt_gen = pkt_utils.RaGenerator(**self.pkt_gen_config) - packet = pkt_gen.generate( - RA_LONG_LIFETIME, enableDNS=True, dns_lifetime=DNS_SHORT_LIFETIME) + packet = pkt_gen.generate(RA_LONG_LIFETIME, + enableDNS=True, + dns_lifetime=DNS_SHORT_LIFETIME) self.sendPacketAndMeasure(packet) @test_tracker_info(uuid='62b99cd7-75bf-45be-b93f-bb037a13b3e2') @@ -342,8 +360,9 @@ class PowerWiFimulticastTest(PWBT.PowerWiFiBaseTest): self.set_connection() self.pkt_gen_config = wputils.create_pkt_config(self) pkt_gen = pkt_utils.RaGenerator(**self.pkt_gen_config) - packet = pkt_gen.generate( - RA_LONG_LIFETIME, enableDNS=True, dns_lifetime=DNS_LONG_LIFETIME) + packet = pkt_gen.generate(RA_LONG_LIFETIME, + enableDNS=True, + dns_lifetime=DNS_LONG_LIFETIME) self.sendPacketAndMeasure(packet) @test_tracker_info(uuid='4088af4c-a64b-4fc1-848c-688936cc6c12') diff --git a/acts/tests/google/power/wifi/PowerWiFiroamingTest.py b/acts/tests/google/power/wifi/PowerWiFiroamingTest.py index 98409895f4..66110a1f34 100644 --- a/acts/tests/google/power/wifi/PowerWiFiroamingTest.py +++ b/acts/tests/google/power/wifi/PowerWiFiroamingTest.py @@ -81,22 +81,30 @@ class PowerWiFiroamingTest(PWBT.PowerWiFiBaseTest): time.sleep(5) # Toggle between two networks begin_time = utils.get_current_epoch_time() + results = [] for i in range(self.toggle_times): self.dut.log.info('Connecting to %s' % network_main[wc.SSID]) self.dut.droid.wifiConnect(network_main) - file_path, avg_current = self.monsoon_data_collect_save() + results.append(self.monsoon_data_collect_save()) self.dut.log.info('Connecting to %s' % network_aux[wc.SSID]) self.dut.droid.wifiConnect(network_aux) - file_path, avg_current = self.monsoon_data_collect_save() - [plot, dt] = wputils.monsoon_data_plot(self.mon_info, file_path) - self.test_result = dt.source.data['y0'][0] - self.power_result.metric_value = ( - self.test_result * PHONE_BATTERY_VOLTAGE) + results.append(self.monsoon_data_collect_save()) + wputils.monsoon_data_plot(self.mon_info, results) + + total_current = 0 + total_samples = 0 + for result in results: + total_current += result.average_current * result.num_samples + total_samples += result.num_samples + average_current = total_current / total_samples + + self.power_result.metric_value = [result.total_power for result in + results] # Take Bugreport if self.bug_report: self.dut.take_bug_report(self.test_name, begin_time) # Path fail check - self.pass_fail_check() + self.pass_fail_check(average_current) @test_tracker_info(uuid='e5ff95c0-b17e-425c-a903-821ba555a9b9') def test_screenon_toggle_between_AP(self): @@ -119,22 +127,30 @@ class PowerWiFiroamingTest(PWBT.PowerWiFiBaseTest): time.sleep(5) # Toggle between two networks begin_time = utils.get_current_epoch_time() + results = [] for i in range(self.toggle_times): self.dut.log.info('Connecting to %s' % network_main[wc.SSID]) self.dut.droid.wifiConnect(network_main) - file_path, avg_current = self.monsoon_data_collect_save() + results.append(self.monsoon_data_collect_save()) self.dut.log.info('Connecting to %s' % network_aux[wc.SSID]) self.dut.droid.wifiConnect(network_aux) - file_path, avg_current = self.monsoon_data_collect_save() - [plot, dt] = wputils.monsoon_data_plot(self.mon_info, file_path) - self.test_result = dt.source.data['y0'][0] - self.power_result.metric_value = ( - self.test_result * PHONE_BATTERY_VOLTAGE) + results.append(self.monsoon_data_collect_save()) + wputils.monsoon_data_plot(self.mon_info, results) + + total_current = 0 + total_samples = 0 + for result in results: + total_current += result.average_current * result.num_samples + total_samples += result.num_samples + average_current = total_current / total_samples + + self.power_result.metric_value = [result.total_power for result in + results] # Take Bugreport if self.bug_report: self.dut.take_bug_report(self.test_name, begin_time) # Path fail check - self.pass_fail_check() + self.pass_fail_check(average_current) @test_tracker_info(uuid='a16ae337-326f-4d09-990f-42232c3c0dc4') def test_screenoff_wifi_wedge(self): diff --git a/acts/tests/google/tel/live/TelLiveDataTest.py b/acts/tests/google/tel/live/TelLiveDataTest.py index 3d967cce40..e1a691069c 100644 --- a/acts/tests/google/tel/live/TelLiveDataTest.py +++ b/acts/tests/google/tel/live/TelLiveDataTest.py @@ -3232,14 +3232,8 @@ class TelLiveDataTest(TelephonyBaseTest): for i in range(1, total_iteration + 1): msg = "Airplane mode test Iteration: <%s> / <%s>" % (i, total_iteration) self.log.info(msg) - if not toggle_airplane_mode(ad.log, ad, True): - ad.log.error("Toggle APM on failed") - fail_count["apm_on"] += 1 - ad.log.error(">----Iteration : %d/%d failed.----<", - i, total_iteration) - if not toggle_airplane_mode(ad.log, ad, False): - ad.log.error("Toggle APM off failed") - fail_count["apm_off"] += 1 + if not airplane_mode_test(self.log, ad): + fail_count["apm_run"] += 1 ad.log.error(">----Iteration : %d/%d failed.----<", i, total_iteration) ad.log.info(">----Iteration : %d/%d succeeded.----<", @@ -3249,11 +3243,12 @@ class TelLiveDataTest(TelephonyBaseTest): for failure, count in fail_count.items(): if count: ad.log.error("%s: %s %s failures in %s iterations", - self.test_name, count, failure, - total_iteration) + self.test_name, count, failure, + total_iteration) test_result = False return test_result + @test_tracker_info(uuid="3a82728f-18b5-4a35-9eab-4e6cf55271d9") @TelephonyBaseTest.tel_test_wrap def test_apm_toggle_stress(self): diff --git a/acts/tests/google/tel/live/TelLiveStressDataTest.py b/acts/tests/google/tel/live/TelLiveStressDataTest.py new file mode 100644 index 0000000000..b8012aeca2 --- /dev/null +++ b/acts/tests/google/tel/live/TelLiveStressDataTest.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +# +# Copyright 2019 - Google +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" + Test Script for Telephony Stress data Test +""" +from acts.test_decorators import test_tracker_info +from acts.test_utils.tel.TelephonyBaseTest import TelephonyBaseTest +from acts.test_utils.tel.tel_test_utils import iperf_test_by_adb +from acts.test_utils.tel.tel_test_utils import iperf_udp_test_by_adb + + +class TelLiveStressDataTest(TelephonyBaseTest): + def setup_class(self): + super().setup_class() + self.ad = self.android_devices[0] + self.iperf_server_address = self.user_params.get("iperf_server", + '0.0.0.0') + self.iperf_srv_tcp_port = self.user_params.get("iperf_server_tcp_port", + 0) + self.iperf_srv_udp_port = self.user_params.get("iperf_server_udp_port", + 0) + self.test_duration = self.user_params.get("data_stress_duration", 60) + + return True + + @test_tracker_info(uuid="190fdeb1-541e-455f-9f37-762a8e55c07f") + @TelephonyBaseTest.tel_test_wrap + def test_tcp_upload_stress(self): + return iperf_test_by_adb(self.log, + self.ad, + self.iperf_server_address, + self.iperf_srv_tcp_port, + False, + self.test_duration) + + @test_tracker_info(uuid="af9805f8-6ed5-4e05-823e-d88dcef45637") + @TelephonyBaseTest.tel_test_wrap + def test_tcp_download_stress(self): + return iperf_test_by_adb(self.log, + self.ad, + self.iperf_server_address, + self.iperf_srv_tcp_port, + True, + self.test_duration) + + @test_tracker_info(uuid="55bf5e09-dc7b-40bc-843f-31fed076ffe4") + @TelephonyBaseTest.tel_test_wrap + def test_udp_upload_stress(self): + return iperf_udp_test_by_adb(self.log, + self.ad, + self.iperf_server_address, + self.iperf_srv_udp_port, + False, + self.test_duration) + + @test_tracker_info(uuid="02ae88b2-d597-45df-ab5a-d701d1125a0f") + @TelephonyBaseTest.tel_test_wrap + def test_udp_download_stress(self): + return iperf_udp_test_by_adb(self.log, + self.ad, + self.iperf_server_address, + self.iperf_srv_udp_port, + True, + self.test_duration) diff --git a/acts/tests/google/tel/live/TelLiveStressTest.py b/acts/tests/google/tel/live/TelLiveStressTest.py index f70fc0880e..ec364ced23 100644 --- a/acts/tests/google/tel/live/TelLiveStressTest.py +++ b/acts/tests/google/tel/live/TelLiveStressTest.py @@ -289,8 +289,8 @@ class TelLiveStressTest(TelephonyBaseTest): 0: sms_send_receive_verify, 1: mms_send_receive_verify } - - self.dut.log.info("Network in RAT %s", self._get_network_rat(slot_id)) + rat = self._get_network_rat(slot_id) + self.dut.log.info("Network in RAT %s", rat) if self.dut_incall and not is_rat_svd_capable(rat.upper()): self.dut.log.info("In call data not supported, test SMS only") selection = 0 @@ -344,7 +344,8 @@ class TelLiveStressTest(TelephonyBaseTest): self.log.error("%s fails", log_msg) self.result_info["%s Failure" % message_type] += 1 else: - self.dut.log.info("Network in RAT %s", self._get_network_rat(slot_id)) + rat = self._get_network_rat(slot_id) + self.dut.log.info("Network in RAT %s", rat) if self.dut_incall and not is_rat_svd_capable(rat.upper()): self.dut.log.info( "In call data not supported, MMS failure expected") @@ -784,8 +785,9 @@ class TelLiveStressTest(TelephonyBaseTest): file_name = file_names[selection] self.result_info["Internet Connection Check Total"] += 1 + rat = self._get_network_rat(slot_id) if not self.internet_connection_check_method(self.log, self.dut): - self.dut.log.info("Network in RAT %s", self._get_network_rat(slot_id)) + self.dut.log.info("Network in RAT %s", rat) if self.dut_incall and not is_rat_svd_capable(rat.upper()): self.result_info[ "Expected Incall Internet Connection Check Failure"] += 1 diff --git a/acts/tests/google/tel/live/TelLiveVoiceTest.py b/acts/tests/google/tel/live/TelLiveVoiceTest.py index fc6aabad2d..5114daa41c 100644 --- a/acts/tests/google/tel/live/TelLiveVoiceTest.py +++ b/acts/tests/google/tel/live/TelLiveVoiceTest.py @@ -56,6 +56,7 @@ from acts.test_utils.tel.tel_test_utils import get_mobile_data_usage from acts.test_utils.tel.tel_test_utils import hangup_call from acts.test_utils.tel.tel_test_utils import initiate_call from acts.test_utils.tel.tel_test_utils import is_phone_in_call_active +from acts.test_utils.tel.tel_test_utils import is_phone_in_call from acts.test_utils.tel.tel_test_utils import multithread_func from acts.test_utils.tel.tel_test_utils import num_active_calls from acts.test_utils.tel.tel_test_utils import remove_mobile_data_usage_limit @@ -68,6 +69,8 @@ from acts.test_utils.tel.tel_test_utils import wait_for_ringing_call from acts.test_utils.tel.tel_test_utils import wait_for_state from acts.test_utils.tel.tel_test_utils import start_youtube_video from acts.test_utils.tel.tel_test_utils import set_wifi_to_default +from acts.test_utils.tel.tel_test_utils import STORY_LINE +from acts.test_utils.tel.tel_test_utils import wait_for_in_call_active from acts.test_utils.tel.tel_voice_utils import is_phone_in_call_1x from acts.test_utils.tel.tel_voice_utils import is_phone_in_call_2g from acts.test_utils.tel.tel_voice_utils import is_phone_in_call_3g @@ -113,6 +116,45 @@ class TelLiveVoiceTest(TelephonyBaseTest): """ Tests Begin """ @TelephonyBaseTest.tel_test_wrap + @test_tracker_info(uuid="c5009f8c-eb1d-4cd9-85ce-604298bbeb3e") + def test_call_to_answering_machine(self): + """ Voice call to an answering machine. + + 1. Make Sure PhoneA attached to voice network. + 2. Call from PhoneA to Storyline + 3. Verify call is in ACTIVE state + 4. Hangup Call from PhoneA + + Raises: + TestFailure if not success. + """ + ad = self.android_devices[0] + + if not phone_setup_voice_general(ad.log, ad): + ad.log.error("Phone Failed to Set Up Properly for Voice.") + return False + for iteration in range(3): + result = True + ad.log.info("Attempt %d", iteration + 1) + if not initiate_call(ad.log, ad, STORY_LINE) and \ + wait_for_in_call_active(ad, 60, 3): + ad.log.error("Call Failed to Initiate") + result = False + time.sleep(WAIT_TIME_IN_CALL) + if not is_phone_in_call(ad.log, ad): + ad.log.error("Call Dropped") + result = False + if not hangup_call(ad.log, ad): + ad.log.error("Call Failed to Hangup") + result = False + if result: + ad.log.info("Call test PASS in iteration %d", iteration + 1) + return True + ad.log.info("Call test FAIL in all 3 iterations") + return False + + + @TelephonyBaseTest.tel_test_wrap @test_tracker_info(uuid="fca3f9e1-447a-416f-9a9c-50b7161981bf") def test_call_mo_voice_general(self): """ General voice to voice call. diff --git a/acts/tests/google/wifi/WifiChaosTest.py b/acts/tests/google/wifi/WifiChaosTest.py index 0fa77b3ae4..e0a4668013 100755 --- a/acts/tests/google/wifi/WifiChaosTest.py +++ b/acts/tests/google/wifi/WifiChaosTest.py @@ -199,8 +199,12 @@ class WifiChaosTest(WifiBaseTest): sec: Time in seconds to run teh ping traffic. """ + self.log.info("Finding Gateway...") + route_response = self.dut.adb.shell("ip route get 8.8.8.8") + gateway_ip = re.search('via (.*) dev', str(route_response)).group(1) + self.log.info("Gateway IP = %s" % gateway_ip) self.log.info("Running ping for %d seconds" % sec) - result = self.dut.adb.shell("ping -w %d %s" % (sec, PING_ADDR), + result = self.dut.adb.shell("ping -w %d %s" % (sec, gateway_ip), timeout=sec + 1) self.log.debug("Ping Result = %s" % result) if "100% packet loss" in result: diff --git a/acts/tests/google/wifi/WifiPingTest.py b/acts/tests/google/wifi/WifiPingTest.py index 8e9bcee24c..1bce90392b 100644 --- a/acts/tests/google/wifi/WifiPingTest.py +++ b/acts/tests/google/wifi/WifiPingTest.py @@ -27,6 +27,7 @@ from acts import utils from acts.controllers.utils_lib import ssh from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger from acts.test_utils.wifi import ota_chamber +from acts.test_utils.wifi import ota_sniffer from acts.test_utils.wifi import wifi_performance_test_utils as wputils from acts.test_utils.wifi import wifi_retail_ap as retail_ap from acts.test_utils.wifi import wifi_test_utils as wutils @@ -77,13 +78,15 @@ class WifiPingTest(base_test.BaseTestClass): 'ping_test_params', 'testbed_params', 'main_network', 'RetailAccessPoints', 'RemoteServer' ] - opt_params = ['golden_files_list'] + opt_params = ['golden_files_list', 'OTASniffer'] self.unpack_userparams(req_params, opt_params) self.testclass_params = self.ping_test_params self.num_atten = self.attenuators[0].instrument.num_atten self.ping_server = ssh.connection.SshConnection( ssh.settings.from_config(self.RemoteServer[0]['ssh_config'])) self.access_point = retail_ap.create(self.RetailAccessPoints)[0] + if hasattr(self, 'OTASniffer'): + self.sniffer = ota_sniffer.create(self.OTASniffer)[0] self.log.info('Access Point Configuration: {}'.format( self.access_point.ap_settings)) self.log_path = os.path.join(logging.log_path, 'results') @@ -325,6 +328,12 @@ class WifiPingTest(base_test.BaseTestClass): test_result['rssi_results'] = [] test_result['ping_results'] = [] test_result['llstats'] = [] + # Setup sniffer + if self.testbed_params['sniffer_enable']: + self.sniffer.start_capture( + testcase_params['test_network'], + testcase_params['ping_duration'] * + len(testcase_params['atten_range']) + self.TEST_TIMEOUT) # Run ping and sweep attenuation as needed zero_counter = 0 for atten in testcase_params['atten_range']: @@ -372,6 +381,8 @@ class WifiPingTest(base_test.BaseTestClass): test_result['ping_results'].append( self.DISCONNECTED_PING_RESULT) break + if self.testbed_params['sniffer_enable']: + self.sniffer.stop_capture() return test_result def setup_ap(self, testcase_params): @@ -411,23 +422,22 @@ class WifiPingTest(base_test.BaseTestClass): asserts.skip('Battery level too low. Skipping test.') # Turn screen off to preserve battery self.dut.go_to_sleep() - band = self.access_point.band_lookup_by_channel( - testcase_params['channel']) current_network = self.dut.droid.wifiGetConnectionInfo() try: connected = wutils.validate_connection(self.dut) is not None except: connected = False - if connected and current_network['SSID'] == self.main_network[band][ - 'SSID']: + if connected and current_network['SSID'] == testcase_params[ + 'test_network']['SSID']: self.log.info('Already connected to desired network') else: wutils.reset_wifi(self.dut) self.dut.droid.wifiSetCountryCode( self.testclass_params['country_code']) - self.main_network[band]['channel'] = testcase_params['channel'] + testcase_params['test_network']['channel'] = testcase_params[ + 'channel'] wutils.wifi_connect(self.dut, - self.main_network[band], + testcase_params['test_network'], num_of_tries=5, check_connectivity=False) self.dut_ip = self.dut.droid.connectivityGetIPv4Addresses('wlan0')[0] @@ -460,6 +470,9 @@ class WifiPingTest(base_test.BaseTestClass): return self.testclass_params['range_atten_start'] def compile_test_params(self, testcase_params): + band = self.access_point.band_lookup_by_channel( + testcase_params['channel']) + testcase_params['test_network'] = self.main_network[band] if testcase_params['test_type'] == 'test_ping_range': testcase_params.update( ping_interval=self.testclass_params['range_ping_interval'], diff --git a/acts/tests/google/wifi/WifiRvrTest.py b/acts/tests/google/wifi/WifiRvrTest.py index cd2ee766c9..289c5e4b27 100644 --- a/acts/tests/google/wifi/WifiRvrTest.py +++ b/acts/tests/google/wifi/WifiRvrTest.py @@ -27,6 +27,7 @@ from acts.controllers import iperf_server as ipf from acts.controllers.utils_lib import ssh from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger from acts.test_utils.wifi import ota_chamber +from acts.test_utils.wifi import ota_sniffer from acts.test_utils.wifi import wifi_performance_test_utils as wputils from acts.test_utils.wifi import wifi_retail_ap as retail_ap from acts.test_utils.wifi import wifi_test_utils as wutils @@ -65,7 +66,7 @@ class WifiRvrTest(base_test.BaseTestClass): 'RetailAccessPoints', 'rvr_test_params', 'testbed_params', 'RemoteServer' ] - opt_params = ['main_network', 'golden_files_list'] + opt_params = ['main_network', 'golden_files_list', 'OTASniffer'] self.unpack_userparams(req_params, opt_params) self.testclass_params = self.rvr_test_params self.num_atten = self.attenuators[0].instrument.num_atten @@ -74,6 +75,8 @@ class WifiRvrTest(base_test.BaseTestClass): ssh.settings.from_config(self.RemoteServer[0]['ssh_config'])) self.iperf_client = self.iperf_clients[0] self.access_point = retail_ap.create(self.RetailAccessPoints)[0] + if hasattr(self, 'OTASniffer'): + self.sniffer = ota_sniffer.create(self.OTASniffer)[0] self.log.info('Access Point Configuration: {}'.format( self.access_point.ap_settings)) self.log_path = os.path.join(logging.log_path, 'results') @@ -311,6 +314,7 @@ class WifiRvrTest(base_test.BaseTestClass): rvr_result['testcase_params']['mode']]['high'] for tput in rvr_result['throughput_receive'] ] + rvr_result['metrics']['high_tput_range'] = -1 for idx in range(len(tput_below_limit)): if all(tput_below_limit[idx:]): if idx == 0: @@ -367,6 +371,11 @@ class WifiRvrTest(base_test.BaseTestClass): attenuator.set_atten(atten, strict=False) # Refresh link layer stats llstats_obj.update_stats() + # Setup sniffer + if self.testbed_params['sniffer_enable']: + self.sniffer.start_capture( + network=testcase_params['test_network'], + duration=self.testclass_params['iperf_duration'] / 5) # Start iperf session self.iperf_server.start(tag=str(atten)) rssi_future = wputils.get_connected_rssi_nb( @@ -383,6 +392,9 @@ class WifiRvrTest(base_test.BaseTestClass): 'chain_1_rssi': rssi_result['chain_1_rssi']['mean'] } rssi.append(current_rssi) + # Stop sniffer + if self.testbed_params['sniffer_enable']: + self.sniffer.stop_capture(tag=str(atten)) # Parse and log result if testcase_params['use_client_output']: iperf_file = client_output_path @@ -407,7 +419,9 @@ class WifiRvrTest(base_test.BaseTestClass): atten, curr_throughput, current_rssi['signal_poll_rssi'], current_rssi['chain_0_rssi'], current_rssi['chain_1_rssi'])) - if curr_throughput == 0 and current_rssi['signal_poll_rssi'] < -80: + if curr_throughput == 0 and ( + current_rssi['signal_poll_rssi'] < -80 + or numpy.isnan(current_rssi['signal_poll_rssi'])): zero_counter = zero_counter + 1 else: zero_counter = 0 @@ -472,17 +486,17 @@ class WifiRvrTest(base_test.BaseTestClass): asserts.skip('Overheating or Battery level low. Skipping test.') # Turn screen off to preserve battery self.dut.go_to_sleep() - band = self.access_point.band_lookup_by_channel( - testcase_params['channel']) - if wputils.validate_network(self.dut, self.main_network[band]['SSID']): + if wputils.validate_network(self.dut, + testcase_params['test_network']['SSID']): self.log.info('Already connected to desired network') else: wutils.reset_wifi(self.dut) self.dut.droid.wifiSetCountryCode( self.testclass_params['country_code']) - self.main_network[band]['channel'] = testcase_params['channel'] + testcase_params['test_network']['channel'] = testcase_params[ + 'channel'] wutils.wifi_connect(self.dut, - self.main_network[band], + testcase_params['test_network'], num_of_tries=5, check_connectivity=True) self.dut_ip = self.dut.droid.connectivityGetIPv4Addresses('wlan0')[0] @@ -522,6 +536,9 @@ class WifiRvrTest(base_test.BaseTestClass): x * self.testclass_params['atten_step'] for x in range(0, num_atten_steps) ] + band = self.access_point.band_lookup_by_channel( + testcase_params['channel']) + testcase_params['test_network'] = self.main_network[band] if (testcase_params['traffic_direction'] == 'DL' and not isinstance(self.iperf_server, ipf.IPerfServerOverAdb) ) or (testcase_params['traffic_direction'] == 'UL' diff --git a/acts/tests/google/wifi/WifiSensitivityTest.py b/acts/tests/google/wifi/WifiSensitivityTest.py index 19476843cd..a982cc6972 100644 --- a/acts/tests/google/wifi/WifiSensitivityTest.py +++ b/acts/tests/google/wifi/WifiSensitivityTest.py @@ -194,7 +194,7 @@ class WifiSensitivityTest(WifiRvrTest, WifiPingTest): Args: result: dict containing attenuation, throughput and other meta - data + data """ try: golden_path = next(file_name @@ -207,7 +207,7 @@ class WifiSensitivityTest(WifiRvrTest, WifiPingTest): except: golden_sensitivity = float('nan') - result_string = ('Througput = {}%, Sensitivity = {}.' + result_string = ('Throughput = {}%, Sensitivity = {}.' 'Target Sensitivity = {}'.format( result['peak_throughput_pct'], result['sensitivity'], golden_sensitivity)) diff --git a/acts/tests/google/wifi/WifiSoftApTest.py b/acts/tests/google/wifi/WifiSoftApTest.py index cfc62d9077..9298e63ae4 100644 --- a/acts/tests/google/wifi/WifiSoftApTest.py +++ b/acts/tests/google/wifi/WifiSoftApTest.py @@ -29,6 +29,7 @@ from acts.test_utils.tel import tel_test_utils as tel_utils from acts.test_utils.tel.tel_test_utils import WIFI_CONFIG_APBAND_2G from acts.test_utils.tel.tel_test_utils import WIFI_CONFIG_APBAND_5G from acts.test_utils.tel.tel_test_utils import WIFI_CONFIG_APBAND_AUTO +from acts.test_utils.wifi import wifi_constants from acts.test_utils.wifi import wifi_test_utils as wutils from acts.test_utils.wifi.WifiBaseTest import WifiBaseTest @@ -486,6 +487,139 @@ class WifiSoftApTest(WifiBaseTest): "No extra android devices. Skip test") self.validate_full_tether_startup(WIFI_CONFIG_APBAND_5G, test_clients=True) + @test_tracker_info(uuid="b991129e-030a-4998-9b08-0687270bec24") + def test_number_of_softap_clients(self): + """Test for number of softap clients to be updated correctly + + 1. Turn of hotspot + 2. Register softap callback + 3. Let client connect to the hotspot + 4. Register second softap callback + 5. Force client connect/disconnect to hotspot + 6. Unregister second softap callback + 7. Force second client connect to hotspot (if supported) + 8. Turn off hotspot + 9. Verify second softap callback doesn't respond after unresister + """ + config = wutils.start_softap_and_verify(self, WIFI_CONFIG_APBAND_AUTO) + # Register callback after softap enabled to avoid unnecessary callback + # impact the test + callbackId = self.dut.droid.registerSoftApCallback() + # Verify clients will update immediately after register callback + wutils.wait_for_expected_number_of_softap_clients( + self.dut, callbackId, 0) + wutils.wait_for_expected_softap_state(self.dut, callbackId, + wifi_constants.WIFI_AP_ENABLED_STATE) + + # Force DUTs connect to Network + wutils.wifi_connect(self.dut_client, config, + check_connectivity=False) + wutils.wait_for_expected_number_of_softap_clients( + self.dut, callbackId, 1) + + # Register another callback to verify multi callback clients case + callbackId_2 = self.dut.droid.registerSoftApCallback() + # Verify clients will update immediately after register callback + wutils.wait_for_expected_number_of_softap_clients( + self.dut, callbackId_2, 1) + wutils.wait_for_expected_softap_state(self.dut, callbackId_2, + wifi_constants.WIFI_AP_ENABLED_STATE) + + # Client Off/On Wifi to verify number of softap clients will be updated + wutils.toggle_wifi_and_wait_for_reconnection(self.dut_client, config) + + wutils.wait_for_expected_number_of_softap_clients(self.dut, + callbackId, 0) + wutils.wait_for_expected_number_of_softap_clients(self.dut, + callbackId_2, 0) + wutils.wait_for_expected_number_of_softap_clients(self.dut, + callbackId, 1) + wutils.wait_for_expected_number_of_softap_clients(self.dut, + callbackId_2, 1) + + # Unregister callbackId_2 to verify multi callback clients case + self.dut.droid.unregisterSoftApCallback(callbackId_2) + + if len(self.android_devices) > 2: + wutils.wifi_connect(self.android_devices[2], config, + check_connectivity=False) + wutils.wait_for_expected_number_of_softap_clients( + self.dut, callbackId, 2) + + # Turn off softap when clients connected + wutils.stop_wifi_tethering(self.dut) + wutils.wait_for_disconnect(self.dut_client) + if len(self.android_devices) > 2: + wutils.wait_for_disconnect(self.android_devices[2]) + + # Verify client number change back to 0 after softap stop if client + # doesn't disconnect before softap stop + wutils.wait_for_expected_softap_state(self.dut, callbackId, + wifi_constants.WIFI_AP_DISABLING_STATE) + wutils.wait_for_expected_softap_state(self.dut, callbackId, + wifi_constants.WIFI_AP_DISABLED_STATE) + wutils.wait_for_expected_number_of_softap_clients( + self.dut, callbackId, 0) + # Unregister callback + self.dut.droid.unregisterSoftApCallback(callbackId) + + # Check no any callbackId_2 event after unregister + asserts.assert_equal( + wutils.get_current_number_of_softap_clients( + self.dut, callbackId_2), None) + + @test_tracker_info(uuid="35bc4ba1-bade-42ee-a563-0c73afb2402a") + def test_softap_auto_shut_off(self): + """Test for softap auto shut off + + 1. Turn of hotspot + 2. Register softap callback + 3. Let client connect to the hotspot + 4. Start wait [wifi_constants.DEFAULT_SOFTAP_TIMEOUT_S] seconds + 5. Check hotspot doesn't shut off + 6. Let client disconnect to the hotspot + 7. Start wait [wifi_constants.DEFAULT_SOFTAP_TIMEOUT_S] seconds + 8. Check hotspot auto shut off + """ + config = wutils.start_softap_and_verify(self, WIFI_CONFIG_APBAND_AUTO) + # Register callback after softap enabled to avoid unnecessary callback + # impact the test + callbackId = self.dut.droid.registerSoftApCallback() + # Verify clients will update immediately after register callback + wutils.wait_for_expected_number_of_softap_clients(self.dut, + callbackId, 0) + wutils.wait_for_expected_softap_state(self.dut, callbackId, + wifi_constants.WIFI_AP_ENABLED_STATE) + + # Force DUTs connect to Network + wutils.wifi_connect(self.dut_client, config, check_connectivity=False) + wutils.wait_for_expected_number_of_softap_clients( + self.dut, callbackId, 1) + + self.dut.log.info("Start waiting %s seconds with 1 clients ", + wifi_constants.DEFAULT_SOFTAP_TIMEOUT_S*1.1) + time.sleep(wifi_constants.DEFAULT_SOFTAP_TIMEOUT_S*1.1) + + # When client connected, softap should keep as enabled + asserts.assert_true(self.dut.droid.wifiIsApEnabled(), + "SoftAp is not reported as running") + + wutils.wifi_toggle_state(self.dut_client, False) + wutils.wait_for_expected_number_of_softap_clients(self.dut, + callbackId, 0) + self.dut.log.info("Start waiting %s seconds with 0 client", + wifi_constants.DEFAULT_SOFTAP_TIMEOUT_S*1.1) + time.sleep(wifi_constants.DEFAULT_SOFTAP_TIMEOUT_S*1.1) + # Softap should stop since no client connected + # doesn't disconnect before softap stop + wutils.wait_for_expected_softap_state(self.dut, callbackId, + wifi_constants.WIFI_AP_DISABLING_STATE) + wutils.wait_for_expected_softap_state(self.dut, callbackId, + wifi_constants.WIFI_AP_DISABLED_STATE) + asserts.assert_false(self.dut.droid.wifiIsApEnabled(), + "SoftAp is not reported as running") + self.dut.droid.unregisterSoftApCallback(callbackId) + """ Tests End """ diff --git a/acts/tests/google/wifi/WifiStressTest.py b/acts/tests/google/wifi/WifiStressTest.py index 40a31083d0..0f9032aaf8 100644 --- a/acts/tests/google/wifi/WifiStressTest.py +++ b/acts/tests/google/wifi/WifiStressTest.py @@ -198,8 +198,8 @@ class WifiStressTest(WifiBaseTest): else: # force start a single scan so we don't have to wait for the scheduled scan. wutils.start_wifi_connection_scan_and_return_status(self.dut) - self.log.info("Wait 20s for network selection.") - time.sleep(20) + self.log.info("Wait 60s for network selection.") + time.sleep(60) try: self.log.info("Connected to %s network after network selection" % self.dut.droid.wifiGetConnectionInfo()) |