1#!/usr/bin/env python3 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 time 19from acts import utils 20from acts.controllers import monsoon 21from acts.libs.proc import job 22from acts.controllers.ap_lib import bridge_interface as bi 23from acts.test_utils.wifi import wifi_test_utils as wutils 24from bokeh.layouts import column, layout 25from bokeh.models import CustomJS, ColumnDataSource 26from bokeh.models import tools as bokeh_tools 27from bokeh.models.widgets import DataTable, TableColumn 28from bokeh.plotting import figure, output_file, save 29from acts.controllers.ap_lib import hostapd_security 30from acts.controllers.ap_lib import hostapd_ap_preset 31 32# http://www.secdev.org/projects/scapy/ 33# On ubuntu, sudo pip3 install scapy 34import scapy.all as scapy 35 36GET_FROM_PHONE = 'get_from_dut' 37GET_FROM_AP = 'get_from_ap' 38ENABLED_MODULATED_DTIM = 'gEnableModulatedDTIM=' 39MAX_MODULATED_DTIM = 'gMaxLIModulatedDTIM=' 40 41 42def monsoon_data_plot(mon_info, file_path, tag=""): 43 """Plot the monsoon current data using bokeh interactive plotting tool. 44 45 Plotting power measurement data with bokeh to generate interactive plots. 46 You can do interactive data analysis on the plot after generating with the 47 provided widgets, which make the debugging much easier. To realize that, 48 bokeh callback java scripting is used. View a sample html output file: 49 https://drive.google.com/open?id=0Bwp8Cq841VnpT2dGUUxLYWZvVjA 50 51 Args: 52 mon_info: obj with information of monsoon measurement, including 53 monsoon device object, measurement frequency, duration and 54 offset etc. 55 file_path: the path to the monsoon log file with current data 56 57 Returns: 58 plot: the plotting object of bokeh, optional, will be needed if multiple 59 plots will be combined to one html file. 60 dt: the datatable object of bokeh, optional, will be needed if multiple 61 datatables will be combined to one html file. 62 """ 63 64 log = logging.getLogger() 65 log.info("Plot the power measurement data") 66 #Get results as monsoon data object from the input file 67 results = monsoon.MonsoonData.from_text_file(file_path) 68 #Decouple current and timestamp data from the monsoon object 69 current_data = [] 70 timestamps = [] 71 voltage = results[0].voltage 72 [current_data.extend(x.data_points) for x in results] 73 [timestamps.extend(x.timestamps) for x in results] 74 period = 1 / float(mon_info.freq) 75 time_relative = [x * period for x in range(len(current_data))] 76 #Calculate the average current for the test 77 current_data = [x * 1000 for x in current_data] 78 avg_current = sum(current_data) / len(current_data) 79 color = ['navy'] * len(current_data) 80 81 #Preparing the data and source link for bokehn java callback 82 source = ColumnDataSource( 83 data=dict(x0=time_relative, y0=current_data, color=color)) 84 s2 = ColumnDataSource( 85 data=dict( 86 z0=[mon_info.duration], 87 y0=[round(avg_current, 2)], 88 x0=[round(avg_current * voltage, 2)], 89 z1=[round(avg_current * voltage * mon_info.duration, 2)], 90 z2=[round(avg_current * mon_info.duration, 2)])) 91 #Setting up data table for the output 92 columns = [ 93 TableColumn(field='z0', title='Total Duration (s)'), 94 TableColumn(field='y0', title='Average Current (mA)'), 95 TableColumn(field='x0', title='Average Power (4.2v) (mW)'), 96 TableColumn(field='z1', title='Average Energy (mW*s)'), 97 TableColumn(field='z2', title='Normalized Average Energy (mA*s)') 98 ] 99 dt = DataTable( 100 source=s2, columns=columns, width=1300, height=60, editable=True) 101 102 plot_title = file_path[file_path.rfind('/') + 1:-4] + tag 103 output_file("%s/%s.html" % (mon_info.data_path, plot_title)) 104 TOOLS = ('box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save') 105 # Create a new plot with the datatable above 106 plot = figure( 107 plot_width=1300, 108 plot_height=700, 109 title=plot_title, 110 tools=TOOLS, 111 output_backend="webgl") 112 plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="width")) 113 plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="height")) 114 plot.line('x0', 'y0', source=source, line_width=2) 115 plot.circle('x0', 'y0', source=source, size=0.5, fill_color='color') 116 plot.xaxis.axis_label = 'Time (s)' 117 plot.yaxis.axis_label = 'Current (mA)' 118 plot.title.text_font_size = {'value': '15pt'} 119 120 #Callback Java scripting 121 source.callback = CustomJS( 122 args=dict(mytable=dt), 123 code=""" 124 var inds = cb_obj.get('selected')['1d'].indices; 125 var d1 = cb_obj.get('data'); 126 var d2 = mytable.get('source').get('data'); 127 ym = 0 128 ts = 0 129 d2['x0'] = [] 130 d2['y0'] = [] 131 d2['z1'] = [] 132 d2['z2'] = [] 133 d2['z0'] = [] 134 min=max=d1['x0'][inds[0]] 135 if (inds.length==0) {return;} 136 for (i = 0; i < inds.length; i++) { 137 ym += d1['y0'][inds[i]] 138 d1['color'][inds[i]] = "red" 139 if (d1['x0'][inds[i]] < min) { 140 min = d1['x0'][inds[i]]} 141 if (d1['x0'][inds[i]] > max) { 142 max = d1['x0'][inds[i]]} 143 } 144 ym /= inds.length 145 ts = max - min 146 dx0 = Math.round(ym*4.2*100.0)/100.0 147 dy0 = Math.round(ym*100.0)/100.0 148 dz1 = Math.round(ym*4.2*ts*100.0)/100.0 149 dz2 = Math.round(ym*ts*100.0)/100.0 150 dz0 = Math.round(ts*1000.0)/1000.0 151 d2['z0'].push(dz0) 152 d2['x0'].push(dx0) 153 d2['y0'].push(dy0) 154 d2['z1'].push(dz1) 155 d2['z2'].push(dz2) 156 mytable.trigger('change'); 157 """) 158 159 #Layout the plot and the datatable bar 160 l = layout([[dt], [plot]]) 161 save(l) 162 return [plot, dt] 163 164 165def change_dtim(ad, gEnableModulatedDTIM, gMaxLIModulatedDTIM=10): 166 """Function to change the DTIM setting in the phone. 167 168 Args: 169 ad: the target android device, AndroidDevice object 170 gEnableModulatedDTIM: Modulated DTIM, int 171 gMaxLIModulatedDTIM: Maximum modulated DTIM, int 172 """ 173 # First trying to find the ini file with DTIM settings 174 ini_file_phone = ad.adb.shell('ls /vendor/firmware/wlan/*/*.ini') 175 ini_file_local = ini_file_phone.split('/')[-1] 176 177 # Pull the file and change the DTIM to desired value 178 ad.adb.pull('{} {}'.format(ini_file_phone, ini_file_local)) 179 180 with open(ini_file_local, 'r') as fin: 181 for line in fin: 182 if ENABLED_MODULATED_DTIM in line: 183 gE_old = line.strip('\n') 184 gEDTIM_old = line.strip(ENABLED_MODULATED_DTIM).strip('\n') 185 if MAX_MODULATED_DTIM in line: 186 gM_old = line.strip('\n') 187 gMDTIM_old = line.strip(MAX_MODULATED_DTIM).strip('\n') 188 fin.close() 189 if int(gEDTIM_old) == gEnableModulatedDTIM and int( 190 gMDTIM_old) == gMaxLIModulatedDTIM: 191 ad.log.info('Current DTIM is already the desired value,' 192 'no need to reset it') 193 return 0 194 195 gE_new = ENABLED_MODULATED_DTIM + str(gEnableModulatedDTIM) 196 gM_new = MAX_MODULATED_DTIM + str(gMaxLIModulatedDTIM) 197 198 sed_gE = 'sed -i \'s/{}/{}/g\' {}'.format(gE_old, gE_new, ini_file_local) 199 sed_gM = 'sed -i \'s/{}/{}/g\' {}'.format(gM_old, gM_new, ini_file_local) 200 job.run(sed_gE) 201 job.run(sed_gM) 202 203 # Push the file to the phone 204 push_file_to_phone(ad, ini_file_local, ini_file_phone) 205 ad.log.info('DTIM changes checked in and rebooting...') 206 ad.reboot() 207 # Wait for auto-wifi feature to start 208 time.sleep(20) 209 ad.adb.shell('dumpsys battery set level 100') 210 ad.log.info('DTIM updated and device back from reboot') 211 return 1 212 213 214def push_file_to_phone(ad, file_local, file_phone): 215 """Function to push local file to android phone. 216 217 Args: 218 ad: the target android device 219 file_local: the locla file to push 220 file_phone: the file/directory on the phone to be pushed 221 """ 222 ad.adb.root() 223 cmd_out = ad.adb.remount() 224 if 'Permission denied' in cmd_out: 225 ad.log.info('Need to disable verity first and reboot') 226 ad.adb.disable_verity() 227 time.sleep(1) 228 ad.reboot() 229 ad.log.info('Verity disabled and device back from reboot') 230 ad.adb.root() 231 ad.adb.remount() 232 time.sleep(1) 233 ad.adb.push('{} {}'.format(file_local, file_phone)) 234 235 236def ap_setup(ap, network, bandwidth=80): 237 """Set up the whirlwind AP with provided network info. 238 239 Args: 240 ap: access_point object of the AP 241 network: dict with information of the network, including ssid, password 242 bssid, channel etc. 243 bandwidth: the operation bandwidth for the AP, default 80MHz 244 Returns: 245 brconfigs: the bridge interface configs 246 """ 247 log = logging.getLogger() 248 bss_settings = [] 249 ssid = network[wutils.WifiEnums.SSID_KEY] 250 if "password" in network.keys(): 251 password = network["password"] 252 security = hostapd_security.Security( 253 security_mode="wpa", password=password) 254 else: 255 security = hostapd_security.Security(security_mode=None, password=None) 256 channel = network["channel"] 257 config = hostapd_ap_preset.create_ap_preset( 258 channel=channel, 259 ssid=ssid, 260 security=security, 261 bss_settings=bss_settings, 262 vht_bandwidth=bandwidth, 263 profile_name='whirlwind', 264 iface_wlan_2g=ap.wlan_2g, 265 iface_wlan_5g=ap.wlan_5g) 266 config_bridge = ap.generate_bridge_configs(channel) 267 brconfigs = bi.BridgeInterfaceConfigs(config_bridge[0], config_bridge[1], 268 config_bridge[2]) 269 ap.bridge.startup(brconfigs) 270 ap.start_ap(config) 271 log.info("AP started on channel {} with SSID {}".format(channel, ssid)) 272 return brconfigs 273 274 275def bokeh_plot(data_sets, 276 legends, 277 fig_property, 278 shaded_region=None, 279 output_file_path=None): 280 """Plot bokeh figs. 281 Args: 282 data_sets: data sets including lists of x_data and lists of y_data 283 ex: [[[x_data1], [x_data2]], [[y_data1],[y_data2]]] 284 legends: list of legend for each curve 285 fig_property: dict containing the plot property, including title, 286 lables, linewidth, circle size, etc. 287 shaded_region: optional dict containing data for plot shading 288 output_file_path: optional path at which to save figure 289 Returns: 290 plot: bokeh plot figure object 291 """ 292 TOOLS = ('box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save') 293 plot = figure( 294 plot_width=1300, 295 plot_height=700, 296 title=fig_property['title'], 297 tools=TOOLS, 298 output_backend="webgl") 299 plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="width")) 300 plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="height")) 301 colors = [ 302 'red', 'green', 'blue', 'olive', 'orange', 'salmon', 'black', 'navy', 303 'yellow', 'darkred', 'goldenrod' 304 ] 305 if shaded_region: 306 band_x = shaded_region["x_vector"] 307 band_x.extend(shaded_region["x_vector"][::-1]) 308 band_y = shaded_region["lower_limit"] 309 band_y.extend(shaded_region["upper_limit"][::-1]) 310 plot.patch( 311 band_x, band_y, color='#7570B3', line_alpha=0.1, fill_alpha=0.1) 312 313 for x_data, y_data, legend in zip(data_sets[0], data_sets[1], legends): 314 index_now = legends.index(legend) 315 color = colors[index_now % len(colors)] 316 plot.line( 317 x_data, 318 y_data, 319 legend=str(legend), 320 line_width=fig_property['linewidth'], 321 color=color) 322 plot.circle( 323 x_data, 324 y_data, 325 size=fig_property['markersize'], 326 legend=str(legend), 327 fill_color=color) 328 329 #Plot properties 330 plot.xaxis.axis_label = fig_property['x_label'] 331 plot.yaxis.axis_label = fig_property['y_label'] 332 plot.legend.location = "top_right" 333 plot.legend.click_policy = "hide" 334 plot.title.text_font_size = {'value': '15pt'} 335 if output_file_path is not None: 336 output_file(output_file_path) 337 save(plot) 338 return plot 339 340 341def save_bokeh_plots(plot_array, output_file_path): 342 all_plots = column(children=plot_array) 343 output_file(output_file_path) 344 save(all_plots) 345 346 347def run_iperf_client_nonblocking(ad, server_host, extra_args=""): 348 """Start iperf client on the device with nohup. 349 350 Return status as true if iperf client start successfully. 351 And data flow information as results. 352 353 Args: 354 ad: the android device under test 355 server_host: Address of the iperf server. 356 extra_args: A string representing extra arguments for iperf client, 357 e.g. "-i 1 -t 30". 358 359 """ 360 log = logging.getLogger() 361 ad.adb.shell_nb("nohup >/dev/null 2>&1 sh -c 'iperf3 -c {} {} &'".format( 362 server_host, extra_args)) 363 log.info("IPerf client started") 364 365 366def get_wifi_rssi(ad): 367 """Get the RSSI of the device. 368 369 Args: 370 ad: the android device under test 371 Returns: 372 RSSI: the rssi level of the device 373 """ 374 RSSI = ad.droid.wifiGetConnectionInfo()['rssi'] 375 return RSSI 376 377 378def get_phone_ip(ad): 379 """Get the WiFi IP address of the phone. 380 381 Args: 382 ad: the android device under test 383 Returns: 384 IP: IP address of the phone for WiFi, as a string 385 """ 386 IP = ad.droid.connectivityGetIPv4Addresses('wlan0')[0] 387 388 return IP 389 390 391def get_phone_mac(ad): 392 """Get the WiFi MAC address of the phone. 393 394 Args: 395 ad: the android device under test 396 Returns: 397 mac: MAC address of the phone for WiFi, as a string 398 """ 399 mac = ad.droid.wifiGetConnectionInfo()["mac_address"] 400 401 return mac 402 403 404def get_phone_ipv6(ad): 405 """Get the WiFi IPV6 address of the phone. 406 407 Args: 408 ad: the android device under test 409 Returns: 410 IPv6: IPv6 address of the phone for WiFi, as a string 411 """ 412 IPv6 = ad.droid.connectivityGetLinkLocalIpv6Address('wlan0')[:-6] 413 414 return IPv6 415 416 417def get_if_addr6(intf, address_type): 418 """Returns the Ipv6 address from a given local interface. 419 420 Returns the desired IPv6 address from the interface 'intf' in human 421 readable form. The address type is indicated by the IPv6 constants like 422 IPV6_ADDR_LINKLOCAL, IPV6_ADDR_GLOBAL, etc. If no address is found, 423 None is returned. 424 425 Args: 426 intf: desired interface name 427 address_type: addrees typle like LINKLOCAL or GLOBAL 428 429 Returns: 430 Ipv6 address of the specified interface in human readable format 431 """ 432 for if_list in scapy.in6_getifaddr(): 433 if if_list[2] == intf and if_list[1] == address_type: 434 return if_list[0] 435 436 return None 437 438 439@utils.timeout(60) 440def wait_for_dhcp(intf): 441 """Wait the DHCP address assigned to desired interface. 442 443 Getting DHCP address takes time and the wait time isn't constant. Utilizing 444 utils.timeout to keep trying until success 445 446 Args: 447 intf: desired interface name 448 Returns: 449 ip: ip address of the desired interface name 450 Raise: 451 TimeoutError: After timeout, if no DHCP assigned, raise 452 """ 453 log = logging.getLogger() 454 reset_host_interface(intf) 455 ip = '0.0.0.0' 456 while ip == '0.0.0.0': 457 ip = scapy.get_if_addr(intf) 458 log.info('DHCP address assigned to {} as {}'.format(intf, ip)) 459 return ip 460 461 462def reset_host_interface(intf): 463 """Reset the host interface. 464 465 Args: 466 intf: the desired interface to reset 467 """ 468 log = logging.getLogger() 469 intf_down_cmd = 'ifconfig %s down' % intf 470 intf_up_cmd = 'ifconfig %s up' % intf 471 try: 472 job.run(intf_down_cmd) 473 time.sleep(10) 474 job.run(intf_up_cmd) 475 log.info('{} has been reset'.format(intf)) 476 except job.Error: 477 raise Exception('No such interface') 478 479 480def create_pkt_config(test_class): 481 """Creates the config for generating multicast packets 482 483 Args: 484 test_class: object with all networking paramters 485 486 Returns: 487 Dictionary with the multicast packet config 488 """ 489 addr_type = (scapy.IPV6_ADDR_LINKLOCAL 490 if test_class.ipv6_src_type == 'LINK_LOCAL' else 491 scapy.IPV6_ADDR_GLOBAL) 492 493 mac_dst = test_class.mac_dst 494 if GET_FROM_PHONE in test_class.mac_dst: 495 mac_dst = get_phone_mac(test_class.dut) 496 497 ipv4_dst = test_class.ipv4_dst 498 if GET_FROM_PHONE in test_class.ipv4_dst: 499 ipv4_dst = get_phone_ip(test_class.dut) 500 501 ipv6_dst = test_class.ipv6_dst 502 if GET_FROM_PHONE in test_class.ipv6_dst: 503 ipv6_dst = get_phone_ipv6(test_class.dut) 504 505 ipv4_gw = test_class.ipv4_gwt 506 if GET_FROM_AP in test_class.ipv4_gwt: 507 ipv4_gw = test_class.access_point.ssh_settings.hostname 508 509 pkt_gen_config = { 510 'interf': test_class.pkt_sender.interface, 511 'subnet_mask': test_class.sub_mask, 512 'src_mac': test_class.mac_src, 513 'dst_mac': mac_dst, 514 'src_ipv4': test_class.ipv4_src, 515 'dst_ipv4': ipv4_dst, 516 'src_ipv6': test_class.ipv6_src, 517 'src_ipv6_type': addr_type, 518 'dst_ipv6': ipv6_dst, 519 'gw_ipv4': ipv4_gw 520 } 521 return pkt_gen_config 522