1#!/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 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 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-python3 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.log.info('DTIM updated and device back from reboot') 210 return 1 211 212 213def push_file_to_phone(ad, file_local, file_phone): 214 """Function to push local file to android phone. 215 216 Args: 217 ad: the target android device 218 file_local: the locla file to push 219 file_phone: the file/directory on the phone to be pushed 220 """ 221 ad.adb.root() 222 cmd_out = ad.adb.remount() 223 if 'Permission denied' in cmd_out: 224 ad.log.info('Need to disable verity first and reboot') 225 ad.adb.disable_verity() 226 time.sleep(1) 227 ad.reboot() 228 ad.log.info('Verity disabled and device back from reboot') 229 ad.adb.root() 230 ad.adb.remount() 231 time.sleep(1) 232 ad.adb.push('{} {}'.format(file_local, file_phone)) 233 234 235def ap_setup(ap, network, bandwidth=80): 236 """Set up the whirlwind AP with provided network info. 237 238 Args: 239 ap: access_point object of the AP 240 network: dict with information of the network, including ssid, password 241 bssid, channel etc. 242 bandwidth: the operation bandwidth for the AP, default 80MHz 243 Returns: 244 brconfigs: the bridge interface configs 245 """ 246 log = logging.getLogger() 247 bss_settings = [] 248 ssid = network[wutils.WifiEnums.SSID_KEY] 249 if "password" in network.keys(): 250 password = network["password"] 251 security = hostapd_security.Security( 252 security_mode="wpa", password=password) 253 else: 254 security = hostapd_security.Security(security_mode=None, password=None) 255 channel = network["channel"] 256 config = hostapd_ap_preset.create_ap_preset( 257 channel=channel, 258 ssid=ssid, 259 security=security, 260 bss_settings=bss_settings, 261 vht_bandwidth=bandwidth, 262 profile_name='whirlwind', 263 iface_wlan_2g=ap.wlan_2g, 264 iface_wlan_5g=ap.wlan_5g) 265 config_bridge = ap.generate_bridge_configs(channel) 266 brconfigs = bi.BridgeInterfaceConfigs(config_bridge[0], config_bridge[1], 267 config_bridge[2]) 268 ap.bridge.startup(brconfigs) 269 ap.start_ap(config) 270 log.info("AP started on channel {} with SSID {}".format(channel, ssid)) 271 return brconfigs 272 273 274def bokeh_plot(data_sets, 275 legends, 276 fig_property, 277 shaded_region=None, 278 output_file_path=None): 279 """Plot bokeh figs. 280 Args: 281 data_sets: data sets including lists of x_data and lists of y_data 282 ex: [[[x_data1], [x_data2]], [[y_data1],[y_data2]]] 283 legends: list of legend for each curve 284 fig_property: dict containing the plot property, including title, 285 lables, linewidth, circle size, etc. 286 shaded_region: optional dict containing data for plot shading 287 output_file_path: optional path at which to save figure 288 Returns: 289 plot: bokeh plot figure object 290 """ 291 TOOLS = ('box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save') 292 plot = figure( 293 plot_width=1300, 294 plot_height=700, 295 title=fig_property['title'], 296 tools=TOOLS, 297 output_backend="webgl") 298 plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="width")) 299 plot.add_tools(bokeh_tools.WheelZoomTool(dimensions="height")) 300 colors = [ 301 'red', 'green', 'blue', 'olive', 'orange', 'salmon', 'black', 'navy', 302 'yellow', 'darkred', 'goldenrod' 303 ] 304 if shaded_region: 305 band_x = shaded_region["x_vector"] 306 band_x.extend(shaded_region["x_vector"][::-1]) 307 band_y = shaded_region["lower_limit"] 308 band_y.extend(shaded_region["upper_limit"][::-1]) 309 plot.patch( 310 band_x, band_y, color='#7570B3', line_alpha=0.1, fill_alpha=0.1) 311 312 for x_data, y_data, legend in zip(data_sets[0], data_sets[1], legends): 313 index_now = legends.index(legend) 314 color = colors[index_now % len(colors)] 315 plot.line( 316 x_data, y_data, legend=str(legend), line_width=fig_property['linewidth'], color=color) 317 plot.circle( 318 x_data, y_data, size=fig_property['markersize'], legend=str(legend), fill_color=color) 319 320 #Plot properties 321 plot.xaxis.axis_label = fig_property['x_label'] 322 plot.yaxis.axis_label = fig_property['y_label'] 323 plot.legend.location = "top_right" 324 plot.legend.click_policy = "hide" 325 plot.title.text_font_size = {'value': '15pt'} 326 if output_file_path is not None: 327 output_file(output_file_path) 328 save(plot) 329 return plot 330 331 332def run_iperf_client_nonblocking(ad, server_host, extra_args=""): 333 """Start iperf client on the device with nohup. 334 335 Return status as true if iperf client start successfully. 336 And data flow information as results. 337 338 Args: 339 ad: the android device under test 340 server_host: Address of the iperf server. 341 extra_args: A string representing extra arguments for iperf client, 342 e.g. "-i 1 -t 30". 343 344 """ 345 log = logging.getLogger() 346 ad.adb.shell_nb("nohup iperf3 -c {} {} &".format(server_host, extra_args)) 347 log.info("IPerf client started") 348 349 350def get_wifi_rssi(ad): 351 """Get the RSSI of the device. 352 353 Args: 354 ad: the android device under test 355 Returns: 356 RSSI: the rssi level of the device 357 """ 358 RSSI = ad.droid.wifiGetConnectionInfo()['rssi'] 359 return RSSI 360 361 362def get_phone_ip(ad): 363 """Get the WiFi IP address of the phone. 364 365 Args: 366 ad: the android device under test 367 Returns: 368 IP: IP address of the phone for WiFi, as a string 369 """ 370 IP = ad.droid.connectivityGetIPv4Addresses('wlan0')[0] 371 372 return IP 373 374 375def get_phone_mac(ad): 376 """Get the WiFi MAC address of the phone. 377 378 Args: 379 ad: the android device under test 380 Returns: 381 mac: MAC address of the phone for WiFi, as a string 382 """ 383 mac = ad.droid.wifiGetConnectionInfo()["mac_address"] 384 385 return mac 386 387 388def get_phone_ipv6(ad): 389 """Get the WiFi IPV6 address of the phone. 390 391 Args: 392 ad: the android device under test 393 Returns: 394 IPv6: IPv6 address of the phone for WiFi, as a string 395 """ 396 IPv6 = ad.droid.connectivityGetLinkLocalIpv6Address('wlan0')[:-6] 397 398 return IPv6 399 400 401def get_if_addr6(intf, address_type): 402 """Returns the Ipv6 address from a given local interface. 403 404 Returns the desired IPv6 address from the interface 'intf' in human 405 readable form. The address type is indicated by the IPv6 constants like 406 IPV6_ADDR_LINKLOCAL, IPV6_ADDR_GLOBAL, etc. If no address is found, 407 None is returned. 408 409 Args: 410 intf: desired interface name 411 address_type: addrees typle like LINKLOCAL or GLOBAL 412 413 Returns: 414 Ipv6 address of the specified interface in human readable format 415 """ 416 for if_list in scapy.in6_getifaddr(): 417 if if_list[2] == intf and if_list[1] == address_type: 418 return if_list[0] 419 420 return None 421 422 423@utils.timeout(60) 424def wait_for_dhcp(intf): 425 """Wait the DHCP address assigned to desired interface. 426 427 Getting DHCP address takes time and the wait time isn't constant. Utilizing 428 utils.timeout to keep trying until success 429 430 Args: 431 intf: desired interface name 432 Returns: 433 ip: ip address of the desired interface name 434 Raise: 435 TimeoutError: After timeout, if no DHCP assigned, raise 436 """ 437 log = logging.getLogger() 438 reset_host_interface(intf) 439 ip = '0.0.0.0' 440 while ip == '0.0.0.0': 441 ip = scapy.get_if_addr(intf) 442 log.info('DHCP address assigned to {} as {}'.format(intf, ip)) 443 return ip 444 445 446def reset_host_interface(intf): 447 """Reset the host interface. 448 449 Args: 450 intf: the desired interface to reset 451 """ 452 log = logging.getLogger() 453 intf_down_cmd = 'ifconfig %s down' % intf 454 intf_up_cmd = 'ifconfig %s up' % intf 455 try: 456 job.run(intf_down_cmd) 457 time.sleep(10) 458 job.run(intf_up_cmd) 459 log.info('{} has been reset'.format(intf)) 460 except job.Error: 461 raise Exception('No such interface') 462 463 464def create_pkt_config(test_class): 465 """Creates the config for generating multicast packets 466 467 Args: 468 test_class: object with all networking paramters 469 470 Returns: 471 Dictionary with the multicast packet config 472 """ 473 addr_type = (scapy.IPV6_ADDR_LINKLOCAL 474 if test_class.ipv6_src_type == 'LINK_LOCAL' else 475 scapy.IPV6_ADDR_GLOBAL) 476 477 mac_dst = test_class.mac_dst 478 if GET_FROM_PHONE in test_class.mac_dst: 479 mac_dst = get_phone_mac(test_class.dut) 480 481 ipv4_dst = test_class.ipv4_dst 482 if GET_FROM_PHONE in test_class.ipv4_dst: 483 ipv4_dst = get_phone_ip(test_class.dut) 484 485 ipv6_dst = test_class.ipv6_dst 486 if GET_FROM_PHONE in test_class.ipv6_dst: 487 ipv6_dst = get_phone_ipv6(test_class.dut) 488 489 ipv4_gw = test_class.ipv4_gwt 490 if GET_FROM_AP in test_class.ipv4_gwt: 491 ipv4_gw = test_class.access_point.ssh_settings.hostname 492 493 pkt_gen_config = { 494 'interf': test_class.pkt_sender.interface, 495 'subnet_mask': test_class.sub_mask, 496 'src_mac': test_class.mac_src, 497 'dst_mac': mac_dst, 498 'src_ipv4': test_class.ipv4_src, 499 'dst_ipv4': ipv4_dst, 500 'src_ipv6': test_class.ipv6_src, 501 'src_ipv6_type': addr_type, 502 'dst_ipv6': ipv6_dst, 503 'gw_ipv4': ipv4_gw 504 } 505 return pkt_gen_config 506