• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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