blob: f932a0b708bc466a6cb26bb30ebf70918ecbc24e [file] [log] [blame]
Qi Jiang73b25352017-08-02 19:56:31 +00001#!/usr/bin/env python3.4
2#
3# Copyright 2017 Google, Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import logging
18import os
19import time
Qi Jiang8f9c7582017-09-06 21:00:14 +000020from acts import asserts
Qi Jiang73b25352017-08-02 19:56:31 +000021from acts import utils
22from acts.controllers import monsoon
23from acts.test_utils.wifi import wifi_test_utils as wutils
Qi Jiang8f9c7582017-09-06 21:00:14 +000024from bokeh.layouts import layout
Qi Jiang73b25352017-08-02 19:56:31 +000025from bokeh.models import CustomJS, ColumnDataSource
26from bokeh.models.widgets import DataTable, TableColumn
Qi Jiang8f9c7582017-09-06 21:00:14 +000027from bokeh.plotting import figure, output_file, save
Qi Jiang73b25352017-08-02 19:56:31 +000028from acts.controllers.ap_lib import hostapd_security
29from acts.controllers.ap_lib import hostapd_ap_preset
Daniel Barrosfefd8c32017-08-04 15:36:40 -070030# http://www.secdev.org/projects/scapy/
31# On ubuntu, sudo pip3 install scapy-python3
32import scapy.all as scapy
Qi Jiang73b25352017-08-02 19:56:31 +000033
34SETTINGS_PAGE = "am start -n com.android.settings/.Settings"
35SCROLL_BOTTOM = "input swipe 0 2000 0 0"
36UNLOCK_SCREEN = "input keyevent 82"
37SCREENON_USB_DISABLE = "dumpsys battery unplug"
38RESET_BATTERY_STATS = "dumpsys batterystats --reset"
39AOD_OFF = "settings put secure doze_always_on 0"
40MUSIC_IQ_OFF = "pm disable-user com.google.intelligence.sense"
41# Command to disable gestures
42LIFT = "settings put secure doze_pulse_on_pick_up 0"
43DOUBLE_TAP = "settings put secure doze_pulse_on_double_tap 0"
44JUMP_TO_CAMERA = "settings put secure camera_double_tap_power_gesture_disabled 1"
45RAISE_TO_CAMERA = "settings put secure camera_lift_trigger_enabled 0"
46FLIP_CAMERA = "settings put secure camera_double_twist_to_flip_enabled 0"
47ASSIST_GESTURE = "settings put secure assist_gesture_enabled 0"
48ASSIST_GESTURE_ALERT = "settings put secure assist_gesture_silence_alerts_enabled 0"
49ASSIST_GESTURE_WAKE = "settings put secure assist_gesture_wake_enabled 0"
50SYSTEM_NAVI = "settings put secure system_navigation_keys_enabled 0"
51# End of command to disable gestures
52AUTO_TIME_OFF = "settings put global auto_time 0"
53AUTO_TIMEZONE_OFF = "settings put global auto_time_zone 0"
Qi Jiang50733382017-08-22 01:11:58 +000054FORCE_YOUTUBE_STOP = "am force-stop com.google.android.youtube"
55FORCE_DIALER_STOP = "am force-stop com.google.android.dialer"
Qi Jiang73b25352017-08-02 19:56:31 +000056IPERF_TIMEOUT = 180
Daniel Barros7cc34e82017-09-22 02:41:32 +000057THRESHOLD_TOLERANCE = 0.2
Qi Jiang73b25352017-08-02 19:56:31 +000058
59
60def dut_rockbottom(ad):
61 """Set the phone into Rock-bottom state.
62
63 Args:
64 ad: the target android device, AndroidDevice object
65
66 """
67 ad.log.info("Now set the device to Rockbottom State")
68 utils.require_sl4a((ad, ))
Daniel Barros7cc34e82017-09-22 02:41:32 +000069 ad.droid.connectivityToggleAirplaneMode(False)
70 time.sleep(5)
71 ad.droid.connectivityToggleAirplaneMode(True)
Qi Jiang73b25352017-08-02 19:56:31 +000072 utils.set_ambient_display(ad, False)
73 utils.set_auto_rotate(ad, False)
74 utils.set_adaptive_brightness(ad, False)
75 utils.sync_device_time(ad)
76 utils.set_location_service(ad, False)
77 utils.set_mobile_data_always_on(ad, False)
78 utils.disable_doze_light(ad)
79 utils.disable_doze(ad)
80 wutils.reset_wifi(ad)
81 wutils.wifi_toggle_state(ad, False)
Qi Jiang73b25352017-08-02 19:56:31 +000082 ad.droid.nfcDisable()
83 ad.droid.setScreenBrightness(0)
84 ad.adb.shell(AOD_OFF)
85 ad.droid.setScreenTimeout(2200)
Qi Jiang73b25352017-08-02 19:56:31 +000086 ad.droid.wakeUpNow()
87 ad.adb.shell(LIFT)
88 ad.adb.shell(DOUBLE_TAP)
89 ad.adb.shell(JUMP_TO_CAMERA)
90 ad.adb.shell(RAISE_TO_CAMERA)
91 ad.adb.shell(FLIP_CAMERA)
92 ad.adb.shell(ASSIST_GESTURE)
93 ad.adb.shell(ASSIST_GESTURE_ALERT)
94 ad.adb.shell(ASSIST_GESTURE_WAKE)
95 ad.adb.shell(SCREENON_USB_DISABLE)
96 ad.adb.shell(UNLOCK_SCREEN)
97 ad.adb.shell(SETTINGS_PAGE)
98 ad.adb.shell(SCROLL_BOTTOM)
99 ad.adb.shell(MUSIC_IQ_OFF)
100 ad.adb.shell(AUTO_TIME_OFF)
101 ad.adb.shell(AUTO_TIMEZONE_OFF)
Qi Jiang50733382017-08-22 01:11:58 +0000102 ad.adb.shell(FORCE_YOUTUBE_STOP)
103 ad.adb.shell(FORCE_DIALER_STOP)
104 ad.droid.wakeUpNow()
Qi Jiang73b25352017-08-02 19:56:31 +0000105 ad.log.info('Device has been set to Rockbottom state')
106
107
Qi Jiang8f9c7582017-09-06 21:00:14 +0000108def pass_fail_check(test_class, test_result):
109 """Check the test result and decide if it passed or failed.
110 The threshold is provided in the config file
111
112 Args:
113 test_class: the specific test class where test is running
114 avg_current: the average current as the test result
115 """
116 test_name = test_class.current_test_name
117 current_threshold = test_class.threshold[test_name]
118 asserts.assert_true(
119 abs(test_result - current_threshold) / current_threshold <
120 THRESHOLD_TOLERANCE,
121 ("Measured average current in [%s]: %s, which is "
122 "more than %d percent off than acceptable threshold %.2fmA") %
123 (test_name, test_result, THRESHOLD_TOLERANCE * 100, current_threshold))
124 asserts.explicit_pass("Measurement finished for %s." % test_name)
125
126
Qi Jiang73b25352017-08-02 19:56:31 +0000127def monsoon_data_collect_save(ad, mon_info, test_name, bug_report):
128 """Current measurement and save the log file.
129
130 Collect current data using Monsoon box and return the path of the
131 log file. Take bug report if requested.
132
133 Args:
134 ad: the android device under test
135 mon_info: dict with information of monsoon measurement, including
136 monsoon device object, measurement frequency, duration and
137 offset etc.
138 test_name: current test name, used to contruct the result file name
139 bug_report: indicator to take bug report or not, 0 or 1
140 Returns:
141 data_path: the absolute path to the log file of monsoon current
142 measurement
143 avg_current: the average current of the test
144 """
145 log = logging.getLogger()
146 log.info("Starting power measurement with monsoon box")
147 tag = (test_name + '_' + ad.model + '_' + ad.build_info['build_id'])
148 #Resets the battery status right before the test started
149 ad.adb.shell(RESET_BATTERY_STATS)
150 begin_time = utils.get_current_human_time()
151 #Start the power measurement using monsoon
152 result = mon_info['dut'].measure_power(
153 mon_info['freq'],
154 mon_info['duration'],
155 tag=tag,
156 offset=mon_info['offset'])
157 data_path = os.path.join(mon_info['data_path'], "%s.txt" % tag)
158 avg_current = result.average_current
159 monsoon.MonsoonData.save_to_text_file([result], data_path)
160 log.info("Power measurement done")
161 if bool(bug_report) == True:
162 ad.take_bug_report(test_name, begin_time)
163 return data_path, avg_current
164
165
166def monsoon_data_plot(mon_info, file_path, tag=""):
167 """Plot the monsoon current data using bokeh interactive plotting tool.
168
169 Plotting power measurement data with bokeh to generate interactive plots.
170 You can do interactive data analysis on the plot after generating with the
171 provided widgets, which make the debugging much easier. To realize that,
172 bokeh callback java scripting is used. View a sample html output file:
173 https://drive.google.com/open?id=0Bwp8Cq841VnpT2dGUUxLYWZvVjA
174
175 Args:
176 mon_info: dict with information of monsoon measurement, including
177 monsoon device object, measurement frequency, duration and
178 offset etc.
179 file_path: the path to the monsoon log file with current data
180
181 Returns:
182 plot: the plotting object of bokeh, optional, will be needed if multiple
183 plots will be combined to one html file.
184 dt: the datatable object of bokeh, optional, will be needed if multiple
185 datatables will be combined to one html file.
186 """
187
188 log = logging.getLogger()
189 log.info("Plot the power measurement data")
190 #Get results as monsoon data object from the input file
191 results = monsoon.MonsoonData.from_text_file(file_path)
192 #Decouple current and timestamp data from the monsoon object
193 current_data = []
194 timestamps = []
195 voltage = results[0].voltage
196 [current_data.extend(x.data_points) for x in results]
197 [timestamps.extend(x.timestamps) for x in results]
198 period = 1 / float(mon_info['freq'])
199 time_relative = [x * period for x in range(len(current_data))]
200 #Calculate the average current for the test
201 current_data = [x * 1000 for x in current_data]
202 avg_current = sum(current_data) / len(current_data)
203 color = ['navy'] * len(current_data)
204
205 #Preparing the data and source link for bokehn java callback
206 source = ColumnDataSource(data=dict(
207 x0=time_relative, y0=current_data, color=color))
208 s2 = ColumnDataSource(data=dict(
209 z0=[mon_info['duration']],
210 y0=[round(avg_current, 2)],
211 x0=[round(avg_current * voltage, 2)],
212 z1=[round(avg_current * voltage * mon_info['duration'], 2)],
213 z2=[round(avg_current * mon_info['duration'], 2)]))
214 #Setting up data table for the output
215 columns = [
216 TableColumn(field='z0', title='Total Duration (s)'),
217 TableColumn(field='y0', title='Average Current (mA)'),
218 TableColumn(field='x0', title='Average Power (4.2v) (mW)'),
219 TableColumn(field='z1', title='Average Energy (mW*s)'),
220 TableColumn(field='z2', title='Normalized Average Energy (mA*s)')
221 ]
222 dt = DataTable(
223 source=s2, columns=columns, width=1300, height=60, editable=True)
224
225 plot_title = file_path[file_path.rfind('/') + 1:-4] + tag
226 output_file("%s/%s.html" % (mon_info['data_path'], plot_title))
227 TOOLS = ('box_zoom,box_select,pan,crosshair,redo,undo,resize,reset,'
228 'hover,xwheel_zoom,ywheel_zoom,save')
229 # Create a new plot with the datatable above
230 plot = figure(
231 plot_width=1300,
232 plot_height=700,
233 title=plot_title,
234 tools=TOOLS,
235 webgl=True)
236 plot.line('x0', 'y0', source=source, line_width=2)
237 plot.circle('x0', 'y0', source=source, size=0.5, fill_color='color')
238 plot.xaxis.axis_label = 'Time (s)'
239 plot.yaxis.axis_label = 'Current (mA)'
240 plot.title.text_font_size = {'value': '15pt'}
241
242 #Callback Java scripting
243 source.callback = CustomJS(
244 args=dict(mytable=dt),
245 code="""
246 var inds = cb_obj.get('selected')['1d'].indices;
247 var d1 = cb_obj.get('data');
248 var d2 = mytable.get('source').get('data');
249 ym = 0
250 ts = 0
251 d2['x0'] = []
252 d2['y0'] = []
253 d2['z1'] = []
254 d2['z2'] = []
255 d2['z0'] = []
256 min=max=d1['x0'][inds[0]]
257 if (inds.length==0) {return;}
258 for (i = 0; i < inds.length; i++) {
259 ym += d1['y0'][inds[i]]
260 d1['color'][inds[i]] = "red"
261 if (d1['x0'][inds[i]] < min) {
262 min = d1['x0'][inds[i]]}
263 if (d1['x0'][inds[i]] > max) {
264 max = d1['x0'][inds[i]]}
265 }
266 ym /= inds.length
267 ts = max - min
268 dx0 = Math.round(ym*4.2*100.0)/100.0
269 dy0 = Math.round(ym*100.0)/100.0
270 dz1 = Math.round(ym*4.2*ts*100.0)/100.0
271 dz2 = Math.round(ym*ts*100.0)/100.0
272 dz0 = Math.round(ts*1000.0)/1000.0
273 d2['z0'].push(dz0)
274 d2['x0'].push(dx0)
275 d2['y0'].push(dy0)
276 d2['z1'].push(dz1)
277 d2['z2'].push(dz2)
278 mytable.trigger('change');
279 """)
280
281 #Layout the plot and the datatable bar
282 l = layout([[dt], [plot]])
283 save(l)
284 return [plot, dt]
285
286
287def change_dtim(ad, gEnableModulatedDTIM, gMaxLIModulatedDTIM=6):
288 """Function to change the DTIM setting in the phone.
289
290 Args:
291 ad: the target android device, AndroidDevice object
292 gEnableModulatedDTIM: Modulated DTIM, int
293 gMaxLIModulatedDTIM: Maximum modulated DTIM, int
294 """
295 serial = ad.serial
296 ini_file_phone = 'vendor/firmware/wlan/qca_cld/WCNSS_qcom_cfg.ini'
297 ini_file_local = 'local_ini_file.ini'
298 ini_pull_cmd = 'adb -s %s pull %s %s' % (serial, ini_file_phone,
299 ini_file_local)
300 ini_push_cmd = 'adb -s %s push %s %s' % (serial, ini_file_local,
301 ini_file_phone)
302 utils.exe_cmd(ini_pull_cmd)
303
304 with open(ini_file_local, 'r') as fin:
305 for line in fin:
306 if 'gEnableModulatedDTIM=' in line:
307 gEDTIM_old = line.strip('gEnableModulatedDTIM=').strip('\n')
308 if 'gMaxLIModulatedDTIM=' in line:
309 gMDTIM_old = line.strip('gMaxLIModulatedDTIM=').strip('\n')
310 if int(gEDTIM_old) == gEnableModulatedDTIM:
311 ad.log.info('Current DTIM is already the desired value,'
312 'no need to reset it')
313 return
314
315 gE_old = 'gEnableModulatedDTIM=' + gEDTIM_old
316 gM_old = 'gMaxLIModulatedDTIM=' + gMDTIM_old
317 gE_new = 'gEnableModulatedDTIM=' + str(gEnableModulatedDTIM)
318 gM_new = 'gMaxLIModulatedDTIM=' + str(gMaxLIModulatedDTIM)
319
320 sed_gE = 'sed -i \'s/%s/%s/g\' %s' % (gE_old, gE_new, ini_file_local)
321 sed_gM = 'sed -i \'s/%s/%s/g\' %s' % (gM_old, gM_new, ini_file_local)
322 utils.exe_cmd(sed_gE)
323 utils.exe_cmd(sed_gM)
324
325 utils.exe_cmd('adb -s {} root'.format(serial))
326 cmd_out = utils.exe_cmd('adb -s {} remount'.format(serial))
327 if ("Permission denied").encode() in cmd_out:
328 ad.log.info('Need to disable verity first and reboot')
329 utils.exe_cmd('adb -s {} disable-verity'.format(serial))
330 time.sleep(1)
331 ad.reboot()
332 ad.log.info('Verity disabled and device back from reboot')
333 utils.exe_cmd('adb -s {} root'.format(serial))
334 utils.exe_cmd('adb -s {} remount'.format(serial))
335 time.sleep(1)
336 utils.exe_cmd(ini_push_cmd)
337 ad.log.info('ini file changes checked in and rebooting...')
338 ad.reboot()
339 ad.log.info('DTIM updated and device back from reboot')
340
341
342def ap_setup(ap, network):
343 """Set up the whirlwind AP with provided network info.
344
345 Args:
346 ap: access_point object of the AP
347 network: dict with information of the network, including ssid, password
348 bssid, channel etc.
349 """
350
351 log = logging.getLogger()
352 bss_settings = []
353 ssid = network[wutils.WifiEnums.SSID_KEY]
354 password = network["password"]
355 channel = network["channel"]
356 security = hostapd_security.Security(
357 security_mode="wpa", password=password)
358 config = hostapd_ap_preset.create_ap_preset(
359 channel=channel,
360 ssid=ssid,
361 security=security,
362 bss_settings=bss_settings,
363 profile_name='whirlwind')
364 ap.start_ap(config)
365 log.info("AP started on channel {} with SSID {}".format(channel, ssid))
366
367
368def bokeh_plot(data_sets, legends, fig_property):
369 """Plot bokeh figs.
370 Args:
371 data_sets: data sets including lists of x_data and lists of y_data
372 ex: [[[x_data1], [x_data2]], [[y_data1],[y_data2]]]
373 legends: list of legend for each curve
374 fig_property: dict containing the plot property, including title,
375 lables, linewidth, circle size, etc.
376 Returns:
377 plot: bokeh plot figure object
378 """
379 TOOLS = ('box_zoom,box_select,pan,crosshair,redo,undo,resize,reset,'
380 'hover,xwheel_zoom,ywheel_zoom,save')
381 plot = figure(
382 plot_width=1300,
383 plot_height=700,
384 title=fig_property['title'],
385 tools=TOOLS,
386 webgl=True)
387 colors = [
388 'red', 'green', 'blue', 'olive', 'orange', 'salmon', 'black', 'navy',
389 'yellow', 'darkred', 'goldenrod'
390 ]
391 for x_data, y_data, legend in zip(data_sets[0], data_sets[1], legends):
392 index_now = legends.index(legend)
393 color = colors[index_now % len(colors)]
394 plot.line(
395 x_data, y_data, legend=str(legend), line_width=3, color=color)
396 plot.circle(
397 x_data, y_data, size=10, legend=str(legend), fill_color=color)
398 #Plot properties
399 plot.xaxis.axis_label = fig_property['x_label']
400 plot.yaxis.axis_label = fig_property['y_label']
401 plot.legend.location = "top_right"
402 plot.legend.click_policy = "hide"
403 plot.title.text_font_size = {'value': '15pt'}
404 return plot
405
406
407def run_iperf_client_nonblocking(ad, server_host, extra_args=""):
408 """Start iperf client on the device with nohup.
409
410 Return status as true if iperf client start successfully.
411 And data flow information as results.
412
413 Args:
414 ad: the android device under test
415 server_host: Address of the iperf server.
416 extra_args: A string representing extra arguments for iperf client,
417 e.g. "-i 1 -t 30".
418
419 """
420 log = logging.getLogger()
421 ad.adb.shell_nb("nohup iperf3 -c {} {} &".format(server_host, extra_args))
422 log.info("IPerf client started")
423
424
425def get_wifi_rssi(ad):
426 """Get the RSSI of the device.
427
428 Args:
429 ad: the android device under test
430 Returns:
431 RSSI: the rssi level of the device
432 """
433 RSSI = ad.droid.wifiGetConnectionInfo()['rssi']
434 return RSSI
Qi Jiang50733382017-08-22 01:11:58 +0000435
436
437def get_phone_ip(ad):
438 """Get the WiFi IP address of the phone.
439
440 Args:
441 ad: the android device under test
442 Returns:
443 IP: IP address of the phone for WiFi, as a string
444 """
445 IP = ad.droid.connectivityGetIPv4Addresses('wlan0')[0]
446
447 return IP
448
449
450def get_phone_mac(ad):
451 """Get the WiFi MAC address of the phone.
452
453 Args:
454 ad: the android device under test
455 Returns:
456 mac: MAC address of the phone for WiFi, as a string
457 """
458 mac = ad.droid.wifiGetConnectionInfo()["mac_address"]
459
460 return mac
461
462
463def get_phone_ipv6(ad):
464 """Get the WiFi IPV6 address of the phone.
465
466 Args:
467 ad: the android device under test
468 Returns:
469 IPv6: IPv6 address of the phone for WiFi, as a string
470 """
471 IPv6 = ad.droid.connectivityGetLinkLocalIpv6Address('wlan0')[:-6]
472
473 return IPv6
Daniel Barrosfefd8c32017-08-04 15:36:40 -0700474
475
476def get_if_addr6(intf, address_type):
477 """Returns the Ipv6 address from a given local interface.
478
479 Returns the desired IPv6 address from the interface 'intf' in human
480 readable form. The address type is indicated by the IPv6 constants like
481 IPV6_ADDR_LINKLOCAL, IPV6_ADDR_GLOBAL, etc. If no address is found,
482 None is returned.
483
484 Args:
485 intf: desired interface name
486 address_type: addrees typle like LINKLOCAL or GLOBAL
487
488 Returns:
489 Ipv6 address of the specified interface in human readable format
490 """
491 for if_list in scapy.in6_getifaddr():
492 if if_list[2] == intf and if_list[1] == address_type:
493 return if_list[0]
494
495 return None