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