blob: 186288944caa872014b68901a190e70ea74fb086 [file] [log] [blame]
Omar El Ayach9badc4b2019-02-04 10:48:39 -08001#!/usr/bin/env python3.4
2#
3# Copyright 2019 - The Android Open Source Project
4#
Omar El Ayachbbc6a0a2019-07-22 10:26:11 -07005# Licensed under the Apache License, Version 2.0 (the 'License');
Omar El Ayach9badc4b2019-02-04 10:48:39 -08006# 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
Omar El Ayachbbc6a0a2019-07-22 10:26:11 -070012# distributed under the License is distributed on an 'AS IS' BASIS,
Omar El Ayach9badc4b2019-02-04 10:48:39 -080013# 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
Omar El Ayach45ea3222020-12-21 22:00:49 -080017import bokeh, bokeh.plotting, bokeh.io
Omar El Ayach9badc4b2019-02-04 10:48:39 -080018import collections
Omar El Ayach08172122020-04-27 12:39:00 -070019import hashlib
Omar El Ayach45ea3222020-12-21 22:00:49 -080020import ipaddress
Omar El Ayacheec95852019-09-26 22:10:09 -070021import itertools
Omar El Ayach69749282019-09-25 16:29:58 -070022import json
Omar El Ayach14416ac2019-01-30 14:58:19 -080023import logging
Omar El Ayach9badc4b2019-02-04 10:48:39 -080024import math
Omar El Ayache5b8be12019-10-01 16:27:12 -070025import os
Omar El Ayach9badc4b2019-02-04 10:48:39 -080026import re
27import statistics
28import time
29from acts.controllers.android_device import AndroidDevice
30from acts.controllers.utils_lib import ssh
Omar El Ayachc2dd2232021-01-13 14:32:55 -080031from acts import asserts
Omar El Ayach74f78dc2019-07-30 15:15:34 -070032from acts import utils
Xianyuan Jia63751fb2020-11-17 00:07:40 +000033from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
Omar El Ayach9badc4b2019-02-04 10:48:39 -080034from concurrent.futures import ThreadPoolExecutor
35
36SHORT_SLEEP = 1
37MED_SLEEP = 6
38TEST_TIMEOUT = 10
39STATION_DUMP = 'iw wlan0 station dump'
40SCAN = 'wpa_cli scan'
41SCAN_RESULTS = 'wpa_cli scan_results'
42SIGNAL_POLL = 'wpa_cli signal_poll'
Omar El Ayachb503ae82019-02-13 09:36:36 -080043WPA_CLI_STATUS = 'wpa_cli status'
Omar El Ayach8abe03b2020-09-24 16:16:54 -070044DISCONNECTION_MESSAGE_BRCM = 'driver adapter not found'
Omar El Ayach9badc4b2019-02-04 10:48:39 -080045CONST_3dB = 3.01029995664
46RSSI_ERROR_VAL = float('nan')
Mark De Ruyterfdc16052019-03-11 13:55:06 -070047RTT_REGEX = re.compile(r'^\[(?P<timestamp>\S+)\] .*? time=(?P<rtt>\S+)')
48LOSS_REGEX = re.compile(r'(?P<loss>\S+)% packet loss')
Omar El Ayacha8c8d122020-03-24 12:48:06 -070049FW_REGEX = re.compile(r'FW:(?P<firmware>\S+) HW:')
Omar El Ayach9badc4b2019-02-04 10:48:39 -080050
51
52# Threading decorator
53def nonblocking(f):
Omar El Ayach14416ac2019-01-30 14:58:19 -080054 """Creates a decorator transforming function calls to non-blocking"""
Omar El Ayach9badc4b2019-02-04 10:48:39 -080055 def wrap(*args, **kwargs):
56 executor = ThreadPoolExecutor(max_workers=1)
57 thread_future = executor.submit(f, *args, **kwargs)
58 # Ensure resources are freed up when executor ruturns or raises
59 executor.shutdown(wait=False)
60 return thread_future
61
62 return wrap
63
64
Omar El Ayach73477702019-08-07 10:12:33 -070065# JSON serializer
66def serialize_dict(input_dict):
67 """Function to serialize dicts to enable JSON output"""
68 output_dict = collections.OrderedDict()
69 for key, value in input_dict.items():
70 output_dict[_serialize_value(key)] = _serialize_value(value)
71 return output_dict
72
73
74def _serialize_value(value):
75 """Function to recursively serialize dict entries to enable JSON output"""
76 if isinstance(value, tuple):
77 return str(value)
78 if isinstance(value, list):
79 return [_serialize_value(x) for x in value]
80 elif isinstance(value, dict):
81 return serialize_dict(value)
82 else:
83 return value
84
85
Omar El Ayach78213c52020-09-24 15:59:26 -070086# Miscellaneous Wifi Utilities
87def extract_sub_dict(full_dict, fields):
88 sub_dict = collections.OrderedDict(
89 (field, full_dict[field]) for field in fields)
90 return sub_dict
91
92
93def validate_network(dut, ssid):
94 """Check that DUT has a valid internet connection through expected SSID
95
96 Args:
97 dut: android device of interest
98 ssid: expected ssid
99 """
100 current_network = dut.droid.wifiGetConnectionInfo()
101 try:
102 connected = wutils.validate_connection(dut) is not None
103 except:
104 connected = False
105 if connected and current_network['SSID'] == ssid:
106 return True
107 else:
108 return False
109
110
111def get_server_address(ssh_connection, dut_ip, subnet_mask):
112 """Get server address on a specific subnet,
113
Omar El Ayach45ea3222020-12-21 22:00:49 -0800114 This function retrieves the LAN or WAN IP of a remote machine used in
115 testing. If subnet_mask is set to 'public' it returns a machines global ip,
116 else it returns the ip belonging to the dut local network given the dut's
117 ip and subnet mask.
Omar El Ayach78213c52020-09-24 15:59:26 -0700118
119 Args:
120 ssh_connection: object representing server for which we want an ip
Omar El Ayach45ea3222020-12-21 22:00:49 -0800121 dut_ip: string in ip address format, i.e., xxx.xxx.xxx.xxx
122 subnet_mask: string representing subnet mask (public for global ip)
Omar El Ayach78213c52020-09-24 15:59:26 -0700123 """
Omar El Ayach78213c52020-09-24 15:59:26 -0700124 ifconfig_out = ssh_connection.run('ifconfig').stdout
125 ip_list = re.findall('inet (?:addr:)?(\d+.\d+.\d+.\d+)', ifconfig_out)
Omar El Ayach45ea3222020-12-21 22:00:49 -0800126 ip_list = [ipaddress.ip_address(ip) for ip in ip_list]
127
128 if subnet_mask == 'public':
129 for ip in ip_list:
130 # is_global is not used to allow for CGNAT ips in 100.x.y.z range
131 if not ip.is_private:
132 return str(ip)
133 else:
134 dut_network = ipaddress.ip_network('{}/{}'.format(dut_ip, subnet_mask),
135 strict=False)
136 for ip in ip_list:
137 if ip in dut_network:
138 return str(ip)
Omar El Ayach78213c52020-09-24 15:59:26 -0700139 logging.error('No IP address found in requested subnet')
140
141
Omar El Ayach9badc4b2019-02-04 10:48:39 -0800142# Plotting Utilities
Omar El Ayachb503ae82019-02-13 09:36:36 -0800143class BokehFigure():
Omar El Ayach2819be62019-04-11 12:00:00 -0700144 """Class enabling simplified Bokeh plotting."""
145
Omar El Ayach270145f2019-04-19 16:12:44 -0700146 COLORS = [
147 'black',
148 'blue',
149 'blueviolet',
150 'brown',
151 'burlywood',
152 'cadetblue',
153 'cornflowerblue',
154 'crimson',
155 'cyan',
156 'darkblue',
157 'darkgreen',
158 'darkmagenta',
159 'darkorange',
160 'darkred',
161 'deepskyblue',
162 'goldenrod',
163 'green',
164 'grey',
165 'indigo',
166 'navy',
167 'olive',
168 'orange',
169 'red',
170 'salmon',
171 'teal',
172 'yellow',
173 ]
174 MARKERS = [
175 'asterisk', 'circle', 'circle_cross', 'circle_x', 'cross', 'diamond',
176 'diamond_cross', 'hex', 'inverted_triangle', 'square', 'square_x',
177 'square_cross', 'triangle', 'x'
178 ]
179
Omar El Ayache140f942020-03-24 10:56:03 -0700180 TOOLS = ('box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save')
181 TOOLTIPS = [
182 ('index', '$index'),
183 ('(x,y)', '($x, $y)'),
184 ('info', '@hover_text'),
185 ]
186
Omar El Ayachb503ae82019-02-13 09:36:36 -0800187 def __init__(self,
188 title=None,
189 x_label=None,
Omar El Ayach954eb282019-09-30 15:33:32 -0700190 primary_y_label=None,
191 secondary_y_label=None,
Omar El Ayachb503ae82019-02-13 09:36:36 -0800192 height=700,
Omar El Ayach0d4358e2020-02-06 15:30:08 -0800193 width=1100,
Omar El Ayach954eb282019-09-30 15:33:32 -0700194 title_size='15pt',
Omar El Ayache140f942020-03-24 10:56:03 -0700195 axis_label_size='12pt',
196 json_file=None):
197 if json_file:
198 self.load_from_json(json_file)
199 else:
200 self.figure_data = []
201 self.fig_property = {
202 'title': title,
203 'x_label': x_label,
204 'primary_y_label': primary_y_label,
205 'secondary_y_label': secondary_y_label,
206 'num_lines': 0,
207 'height': height,
208 'width': width,
209 'title_size': title_size,
210 'axis_label_size': axis_label_size
211 }
Omar El Ayachbbc6a0a2019-07-22 10:26:11 -0700212
213 def init_plot(self):
Omar El Ayachb503ae82019-02-13 09:36:36 -0800214 self.plot = bokeh.plotting.figure(
Omar El Ayachfa43c1a2020-04-07 19:29:56 -0700215 sizing_mode='scale_both',
Omar El Ayachbbc6a0a2019-07-22 10:26:11 -0700216 plot_width=self.fig_property['width'],
217 plot_height=self.fig_property['height'],
218 title=self.fig_property['title'],
Omar El Ayachb503ae82019-02-13 09:36:36 -0800219 tools=self.TOOLS,
220 output_backend='webgl')
Omar El Ayach26d4fab2019-07-02 18:10:58 -0700221 self.plot.hover.tooltips = self.TOOLTIPS
Omar El Ayachb503ae82019-02-13 09:36:36 -0800222 self.plot.add_tools(
223 bokeh.models.tools.WheelZoomTool(dimensions='width'))
224 self.plot.add_tools(
225 bokeh.models.tools.WheelZoomTool(dimensions='height'))
226
Omar El Ayacheec95852019-09-26 22:10:09 -0700227 def _filter_line(self, x_data, y_data, hover_text=None):
228 """Function to remove NaN points from bokeh plots."""
229 x_data_filtered = []
230 y_data_filtered = []
231 hover_text_filtered = []
232 for x, y, hover in itertools.zip_longest(x_data, y_data, hover_text):
233 if not math.isnan(y):
234 x_data_filtered.append(x)
235 y_data_filtered.append(y)
236 hover_text_filtered.append(hover)
237 return x_data_filtered, y_data_filtered, hover_text_filtered
238
Omar El Ayachb503ae82019-02-13 09:36:36 -0800239 def add_line(self,
240 x_data,
241 y_data,
242 legend,
Omar El Ayachf6729b02019-06-28 18:18:43 -0700243 hover_text=None,
Omar El Ayachb503ae82019-02-13 09:36:36 -0800244 color=None,
245 width=3,
246 style='solid',
247 marker=None,
248 marker_size=10,
249 shaded_region=None,
250 y_axis='default'):
Omar El Ayach2819be62019-04-11 12:00:00 -0700251 """Function to add line to existing BokehFigure.
252
253 Args:
254 x_data: list containing x-axis values for line
255 y_data: list containing y_axis values for line
256 legend: string containing line title
Omar El Ayachf6729b02019-06-28 18:18:43 -0700257 hover_text: text to display when hovering over lines
Omar El Ayach2819be62019-04-11 12:00:00 -0700258 color: string describing line color
259 width: integer line width
Omar El Ayach270145f2019-04-19 16:12:44 -0700260 style: string describing line style, e.g, solid or dashed
Omar El Ayach2819be62019-04-11 12:00:00 -0700261 marker: string specifying line marker, e.g., cross
262 shaded region: data describing shaded region to plot
263 y_axis: identifier for y-axis to plot line against
264 """
Omar El Ayachb503ae82019-02-13 09:36:36 -0800265 if y_axis not in ['default', 'secondary']:
266 raise ValueError('y_axis must be default or secondary')
267 if color == None:
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700268 color = self.COLORS[self.fig_property['num_lines'] %
269 len(self.COLORS)]
Omar El Ayachb503ae82019-02-13 09:36:36 -0800270 if style == 'dashed':
271 style = [5, 5]
Omar El Ayachf6729b02019-06-28 18:18:43 -0700272 if not hover_text:
Omar El Ayachbbc6a0a2019-07-22 10:26:11 -0700273 hover_text = ['y={}'.format(y) for y in y_data]
Omar El Ayacheec95852019-09-26 22:10:09 -0700274 x_data_filter, y_data_filter, hover_text_filter = self._filter_line(
275 x_data, y_data, hover_text)
Omar El Ayachb503ae82019-02-13 09:36:36 -0800276 self.figure_data.append({
Omar El Ayacheec95852019-09-26 22:10:09 -0700277 'x_data': x_data_filter,
278 'y_data': y_data_filter,
Omar El Ayachb503ae82019-02-13 09:36:36 -0800279 'legend': legend,
Omar El Ayacheec95852019-09-26 22:10:09 -0700280 'hover_text': hover_text_filter,
Omar El Ayachb503ae82019-02-13 09:36:36 -0800281 'color': color,
282 'width': width,
283 'style': style,
284 'marker': marker,
285 'marker_size': marker_size,
286 'shaded_region': shaded_region,
Omar El Ayach954eb282019-09-30 15:33:32 -0700287 'y_axis': y_axis
Omar El Ayachb503ae82019-02-13 09:36:36 -0800288 })
289 self.fig_property['num_lines'] += 1
290
Omar El Ayachc9c51562019-07-17 16:03:56 -0700291 def add_scatter(self,
Omar El Ayachbbc6a0a2019-07-22 10:26:11 -0700292 x_data,
293 y_data,
294 legend,
295 hover_text=None,
296 color=None,
297 marker=None,
298 marker_size=10,
299 y_axis='default'):
Omar El Ayachc9c51562019-07-17 16:03:56 -0700300 """Function to add line to existing BokehFigure.
301
302 Args:
303 x_data: list containing x-axis values for line
304 y_data: list containing y_axis values for line
305 legend: string containing line title
306 hover_text: text to display when hovering over lines
307 color: string describing line color
308 marker: string specifying marker, e.g., cross
309 y_axis: identifier for y-axis to plot line against
310 """
311 if y_axis not in ['default', 'secondary']:
312 raise ValueError('y_axis must be default or secondary')
313 if color == None:
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700314 color = self.COLORS[self.fig_property['num_lines'] %
315 len(self.COLORS)]
Omar El Ayachc9c51562019-07-17 16:03:56 -0700316 if marker == None:
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700317 marker = self.MARKERS[self.fig_property['num_lines'] %
318 len(self.MARKERS)]
Omar El Ayachc9c51562019-07-17 16:03:56 -0700319 if not hover_text:
Omar El Ayachbbc6a0a2019-07-22 10:26:11 -0700320 hover_text = ['y={}'.format(y) for y in y_data]
Omar El Ayachc9c51562019-07-17 16:03:56 -0700321 self.figure_data.append({
322 'x_data': x_data,
323 'y_data': y_data,
324 'legend': legend,
325 'hover_text': hover_text,
326 'color': color,
327 'width': 0,
Omar El Ayachbbc6a0a2019-07-22 10:26:11 -0700328 'style': 'solid',
Omar El Ayachc9c51562019-07-17 16:03:56 -0700329 'marker': marker,
330 'marker_size': marker_size,
331 'shaded_region': None,
Omar El Ayach954eb282019-09-30 15:33:32 -0700332 'y_axis': y_axis
Omar El Ayachc9c51562019-07-17 16:03:56 -0700333 })
334 self.fig_property['num_lines'] += 1
335
Omar El Ayach92442ad2020-05-13 11:37:19 -0700336 def generate_figure(self, output_file=None, save_json=True):
Omar El Ayach2819be62019-04-11 12:00:00 -0700337 """Function to generate and save BokehFigure.
338
339 Args:
340 output_file: string specifying output file path
341 """
Omar El Ayachbbc6a0a2019-07-22 10:26:11 -0700342 self.init_plot()
Omar El Ayachb503ae82019-02-13 09:36:36 -0800343 two_axes = False
344 for line in self.figure_data:
Omar El Ayachf6729b02019-06-28 18:18:43 -0700345 source = bokeh.models.ColumnDataSource(
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700346 data=dict(x=line['x_data'],
347 y=line['y_data'],
348 hover_text=line['hover_text']))
Omar El Ayachc9c51562019-07-17 16:03:56 -0700349 if line['width'] > 0:
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700350 self.plot.line(x='x',
351 y='y',
Omar El Ayachcb10ead2019-12-06 19:42:06 -0800352 legend_label=line['legend'],
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700353 line_width=line['width'],
354 color=line['color'],
355 line_dash=line['style'],
356 name=line['y_axis'],
357 y_range_name=line['y_axis'],
358 source=source)
Omar El Ayachb503ae82019-02-13 09:36:36 -0800359 if line['shaded_region']:
360 band_x = line['shaded_region']['x_vector']
361 band_x.extend(line['shaded_region']['x_vector'][::-1])
362 band_y = line['shaded_region']['lower_limit']
363 band_y.extend(line['shaded_region']['upper_limit'][::-1])
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700364 self.plot.patch(band_x,
365 band_y,
366 color='#7570B3',
367 line_alpha=0.1,
368 fill_alpha=0.1)
Omar El Ayachb503ae82019-02-13 09:36:36 -0800369 if line['marker'] in self.MARKERS:
370 marker_func = getattr(self.plot, line['marker'])
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700371 marker_func(x='x',
372 y='y',
373 size=line['marker_size'],
Omar El Ayachcb10ead2019-12-06 19:42:06 -0800374 legend_label=line['legend'],
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700375 line_color=line['color'],
376 fill_color=line['color'],
377 name=line['y_axis'],
378 y_range_name=line['y_axis'],
379 source=source)
Omar El Ayach954eb282019-09-30 15:33:32 -0700380 if line['y_axis'] == 'secondary':
Omar El Ayachb503ae82019-02-13 09:36:36 -0800381 two_axes = True
382
383 #x-axis formatting
384 self.plot.xaxis.axis_label = self.fig_property['x_label']
385 self.plot.x_range.range_padding = 0
386 self.plot.xaxis[0].axis_label_text_font_size = self.fig_property[
387 'axis_label_size']
388 #y-axis formatting
389 self.plot.yaxis[0].axis_label = self.fig_property['primary_y_label']
390 self.plot.yaxis[0].axis_label_text_font_size = self.fig_property[
391 'axis_label_size']
392 self.plot.y_range = bokeh.models.DataRange1d(names=['default'])
393 if two_axes and 'secondary' not in self.plot.extra_y_ranges:
394 self.plot.extra_y_ranges = {
395 'secondary': bokeh.models.DataRange1d(names=['secondary'])
396 }
397 self.plot.add_layout(
398 bokeh.models.LinearAxis(
399 y_range_name='secondary',
400 axis_label=self.fig_property['secondary_y_label'],
401 axis_label_text_font_size=self.
402 fig_property['axis_label_size']), 'right')
403 # plot formatting
404 self.plot.legend.location = 'top_right'
405 self.plot.legend.click_policy = 'hide'
406 self.plot.title.text_font_size = self.fig_property['title_size']
407
408 if output_file is not None:
Omar El Ayach5179fae2020-05-05 18:49:52 -0700409 self.save_figure(output_file, save_json)
Omar El Ayachb503ae82019-02-13 09:36:36 -0800410 return self.plot
411
Omar El Ayache140f942020-03-24 10:56:03 -0700412 def load_from_json(self, file_path):
413 with open(file_path, 'r') as json_file:
414 fig_dict = json.load(json_file)
415 self.fig_property = fig_dict['fig_property']
416 self.figure_data = fig_dict['figure_data']
417
Omar El Ayach69749282019-09-25 16:29:58 -0700418 def _save_figure_json(self, output_file):
419 """Function to save a json format of a figure"""
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700420 figure_dict = collections.OrderedDict(fig_property=self.fig_property,
Omar El Ayache140f942020-03-24 10:56:03 -0700421 figure_data=self.figure_data)
Omar El Ayach69749282019-09-25 16:29:58 -0700422 output_file = output_file.replace('.html', '_plot_data.json')
423 with open(output_file, 'w') as outfile:
424 json.dump(figure_dict, outfile, indent=4)
425
Omar El Ayach92442ad2020-05-13 11:37:19 -0700426 def save_figure(self, output_file, save_json=True):
Omar El Ayach2819be62019-04-11 12:00:00 -0700427 """Function to save BokehFigure.
428
429 Args:
430 output_file: string specifying output file path
Omar El Ayach5179fae2020-05-05 18:49:52 -0700431 save_json: flag controlling json outputs
Omar El Ayach2819be62019-04-11 12:00:00 -0700432 """
Omar El Ayach5179fae2020-05-05 18:49:52 -0700433 if save_json:
434 self._save_figure_json(output_file)
Omar El Ayach45ea3222020-12-21 22:00:49 -0800435 bokeh.io.output_file(output_file)
436 bokeh.io.save(self.plot)
Omar El Ayachb503ae82019-02-13 09:36:36 -0800437
Omar El Ayach383d61f2019-04-05 17:22:02 -0700438 @staticmethod
Omar El Ayach92442ad2020-05-13 11:37:19 -0700439 def save_figures(figure_array, output_file_path, save_json=True):
Omar El Ayach2819be62019-04-11 12:00:00 -0700440 """Function to save list of BokehFigures in one file.
441
442 Args:
443 figure_array: list of BokehFigure object to be plotted
444 output_file: string specifying output file path
445 """
Omar El Ayach69749282019-09-25 16:29:58 -0700446 for idx, figure in enumerate(figure_array):
Omar El Ayachbbc6a0a2019-07-22 10:26:11 -0700447 figure.generate_figure()
Omar El Ayach5179fae2020-05-05 18:49:52 -0700448 if save_json:
449 json_file_path = output_file_path.replace(
Omar El Ayach92442ad2020-05-13 11:37:19 -0700450 '.html', '{}-plot_data.json'.format(idx))
Omar El Ayach5179fae2020-05-05 18:49:52 -0700451 figure._save_figure_json(json_file_path)
Omar El Ayach383d61f2019-04-05 17:22:02 -0700452 plot_array = [figure.plot for figure in figure_array]
Omar El Ayach0d4358e2020-02-06 15:30:08 -0800453 all_plots = bokeh.layouts.column(children=plot_array,
454 sizing_mode='scale_width')
Omar El Ayach383d61f2019-04-05 17:22:02 -0700455 bokeh.plotting.output_file(output_file_path)
456 bokeh.plotting.save(all_plots)
457
Omar El Ayachb503ae82019-02-13 09:36:36 -0800458
Omar El Ayach73477702019-08-07 10:12:33 -0700459# Ping utilities
Mark De Ruyterfdc16052019-03-11 13:55:06 -0700460class PingResult(object):
461 """An object that contains the results of running ping command.
462
463 Attributes:
464 connected: True if a connection was made. False otherwise.
465 packet_loss_percentage: The total percentage of packets lost.
466 transmission_times: The list of PingTransmissionTimes containing the
467 timestamps gathered for transmitted packets.
468 rtts: An list-like object enumerating all round-trip-times of
469 transmitted packets.
470 timestamps: A list-like object enumerating the beginning timestamps of
471 each packet transmission.
472 ping_interarrivals: A list-like object enumerating the amount of time
473 between the beginning of each subsequent transmission.
474 """
Omar El Ayachb503ae82019-02-13 09:36:36 -0800475 def __init__(self, ping_output):
476 self.packet_loss_percentage = 100
Mark De Ruyterfdc16052019-03-11 13:55:06 -0700477 self.transmission_times = []
478
479 self.rtts = _ListWrap(self.transmission_times, lambda entry: entry.rtt)
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700480 self.timestamps = _ListWrap(self.transmission_times,
481 lambda entry: entry.timestamp)
Mark De Ruyterfdc16052019-03-11 13:55:06 -0700482 self.ping_interarrivals = _PingInterarrivals(self.transmission_times)
483
Omar El Ayach73477702019-08-07 10:12:33 -0700484 self.start_time = 0
Mark De Ruyterfdc16052019-03-11 13:55:06 -0700485 for line in ping_output:
486 if 'loss' in line:
487 match = re.search(LOSS_REGEX, line)
488 self.packet_loss_percentage = float(match.group('loss'))
489 if 'time=' in line:
490 match = re.search(RTT_REGEX, line)
Omar El Ayach73477702019-08-07 10:12:33 -0700491 if self.start_time == 0:
492 self.start_time = float(match.group('timestamp'))
Omar El Ayachb503ae82019-02-13 09:36:36 -0800493 self.transmission_times.append(
494 PingTransmissionTimes(
Omar El Ayach73477702019-08-07 10:12:33 -0700495 float(match.group('timestamp')) - self.start_time,
Omar El Ayachb503ae82019-02-13 09:36:36 -0800496 float(match.group('rtt'))))
497 self.connected = len(
498 ping_output) > 1 and self.packet_loss_percentage < 100
Mark De Ruyterfdc16052019-03-11 13:55:06 -0700499
500 def __getitem__(self, item):
501 if item == 'rtt':
502 return self.rtts
503 if item == 'connected':
504 return self.connected
505 if item == 'packet_loss_percentage':
506 return self.packet_loss_percentage
507 raise ValueError('Invalid key. Please use an attribute instead.')
508
509 def as_dict(self):
510 return {
511 'connected': 1 if self.connected else 0,
512 'rtt': list(self.rtts),
513 'time_stamp': list(self.timestamps),
514 'ping_interarrivals': list(self.ping_interarrivals),
515 'packet_loss_percentage': self.packet_loss_percentage
516 }
517
518
519class PingTransmissionTimes(object):
520 """A class that holds the timestamps for a packet sent via the ping command.
521
522 Attributes:
523 rtt: The round trip time for the packet sent.
524 timestamp: The timestamp the packet started its trip.
525 """
526 def __init__(self, timestamp, rtt):
527 self.rtt = rtt
528 self.timestamp = timestamp
529
530
531class _ListWrap(object):
532 """A convenient helper class for treating list iterators as native lists."""
Mark De Ruyterfdc16052019-03-11 13:55:06 -0700533 def __init__(self, wrapped_list, func):
534 self.__wrapped_list = wrapped_list
535 self.__func = func
536
537 def __getitem__(self, key):
538 return self.__func(self.__wrapped_list[key])
539
540 def __iter__(self):
541 for item in self.__wrapped_list:
542 yield self.__func(item)
543
544 def __len__(self):
545 return len(self.__wrapped_list)
546
547
548class _PingInterarrivals(object):
549 """A helper class for treating ping interarrivals as a native list."""
Mark De Ruyterfdc16052019-03-11 13:55:06 -0700550 def __init__(self, ping_entries):
551 self.__ping_entries = ping_entries
552
553 def __getitem__(self, key):
Omar El Ayachb503ae82019-02-13 09:36:36 -0800554 return (self.__ping_entries[key + 1].timestamp -
555 self.__ping_entries[key].timestamp)
Mark De Ruyterfdc16052019-03-11 13:55:06 -0700556
557 def __iter__(self):
558 for index in range(len(self.__ping_entries) - 1):
559 yield self[index]
560
561 def __len__(self):
Omar El Ayachb503ae82019-02-13 09:36:36 -0800562 return max(0, len(self.__ping_entries) - 1)
Omar El Ayach9badc4b2019-02-04 10:48:39 -0800563
564
565def get_ping_stats(src_device, dest_address, ping_duration, ping_interval,
566 ping_size):
567 """Run ping to or from the DUT.
568
569 The function computes either pings the DUT or pings a remote ip from
570 DUT.
571
572 Args:
Omar El Ayach14416ac2019-01-30 14:58:19 -0800573 src_device: object representing device to ping from
574 dest_address: ip address to ping
Omar El Ayach9badc4b2019-02-04 10:48:39 -0800575 ping_duration: timeout to set on the the ping process (in seconds)
576 ping_interval: time between pings (in seconds)
577 ping_size: size of ping packet payload
578 Returns:
579 ping_result: dict containing ping results and other meta data
580 """
Omar El Ayach69749282019-09-25 16:29:58 -0700581 ping_count = int(ping_duration / ping_interval)
582 ping_deadline = int(ping_count * ping_interval) + 1
Omar El Ayach8e40e5e2020-10-16 11:20:26 -0700583 ping_cmd_linux = 'ping -c {} -w {} -i {} -s {} -D'.format(
Omar El Ayachd94389f2019-09-17 11:25:31 -0700584 ping_count,
585 ping_deadline,
Omar El Ayach9badc4b2019-02-04 10:48:39 -0800586 ping_interval,
587 ping_size,
588 )
Omar El Ayach8e40e5e2020-10-16 11:20:26 -0700589
590 ping_cmd_macos = 'ping -c {} -t {} -i {} -s {}'.format(
591 ping_count,
592 ping_deadline,
593 ping_interval,
594 ping_size,
595 )
596
Omar El Ayach9badc4b2019-02-04 10:48:39 -0800597 if isinstance(src_device, AndroidDevice):
Omar El Ayach8e40e5e2020-10-16 11:20:26 -0700598 ping_cmd = '{} {}'.format(ping_cmd_linux, dest_address)
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700599 ping_output = src_device.adb.shell(ping_cmd,
600 timeout=ping_deadline + SHORT_SLEEP,
601 ignore_status=True)
Omar El Ayach9badc4b2019-02-04 10:48:39 -0800602 elif isinstance(src_device, ssh.connection.SshConnection):
Omar El Ayach8e40e5e2020-10-16 11:20:26 -0700603 platform = src_device.run('uname').stdout
604 if 'linux' in platform.lower():
605 ping_cmd = 'sudo {} {}'.format(ping_cmd_linux, dest_address)
606 elif 'darwin' in platform.lower():
Omar El Ayach229abd72020-11-02 17:25:23 -0800607 ping_cmd = "sudo {} {}| while IFS= read -r line; do printf '[%s] %s\n' \"$(gdate '+%s.%N')\" \"$line\"; done".format(
608 ping_cmd_macos, dest_address)
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700609 ping_output = src_device.run(ping_cmd,
610 timeout=ping_deadline + SHORT_SLEEP,
611 ignore_status=True).stdout
Omar El Ayach9badc4b2019-02-04 10:48:39 -0800612 else:
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700613 raise TypeError('Unable to ping using src_device of type %s.' %
614 type(src_device))
Mark De Ruyterfdc16052019-03-11 13:55:06 -0700615 return PingResult(ping_output.splitlines())
Omar El Ayach9badc4b2019-02-04 10:48:39 -0800616
617
618@nonblocking
619def get_ping_stats_nb(src_device, dest_address, ping_duration, ping_interval,
620 ping_size):
621 return get_ping_stats(src_device, dest_address, ping_duration,
622 ping_interval, ping_size)
623
624
Omar El Ayach15effa32020-06-22 15:28:19 -0700625# Iperf utilities
Omar El Ayachb503ae82019-02-13 09:36:36 -0800626@nonblocking
627def start_iperf_client_nb(iperf_client, iperf_server_address, iperf_args, tag,
628 timeout):
629 return iperf_client.start(iperf_server_address, iperf_args, tag, timeout)
630
631
Omar El Ayach15effa32020-06-22 15:28:19 -0700632def get_iperf_arg_string(duration,
633 reverse_direction,
634 interval=1,
635 traffic_type='TCP',
Omar El Ayach07be7fb2020-09-01 13:50:46 -0700636 socket_size=None,
637 num_processes=1,
Omar El Ayach15effa32020-06-22 15:28:19 -0700638 udp_throughput='1000M',
639 ipv6=False):
640 """Function to format iperf client arguments.
641
642 This function takes in iperf client parameters and returns a properly
643 formatter iperf arg string to be used in throughput tests.
644
645 Args:
646 duration: iperf duration in seconds
647 reverse_direction: boolean controlling the -R flag for iperf clients
648 interval: iperf print interval
649 traffic_type: string specifying TCP or UDP traffic
Omar El Ayach07be7fb2020-09-01 13:50:46 -0700650 socket_size: string specifying TCP window or socket buffer, e.g., 2M
651 num_processes: int specifying number of iperf processes
Omar El Ayach15effa32020-06-22 15:28:19 -0700652 udp_throughput: string specifying TX throughput in UDP tests, e.g. 100M
653 ipv6: boolean controlling the use of IP V6
654 Returns:
655 iperf_args: string of formatted iperf args
656 """
657 iperf_args = '-i {} -t {} -J '.format(interval, duration)
658 if ipv6:
659 iperf_args = iperf_args + '-6 '
660 if traffic_type.upper() == 'UDP':
Omar El Ayachd4694892020-12-04 14:24:09 -0800661 iperf_args = iperf_args + '-u -b {} -l 1470 -P {} '.format(
Omar El Ayach07be7fb2020-09-01 13:50:46 -0700662 udp_throughput, num_processes)
Omar El Ayach15effa32020-06-22 15:28:19 -0700663 elif traffic_type.upper() == 'TCP':
Omar El Ayach6f8b62c2020-09-03 09:08:09 -0700664 iperf_args = iperf_args + '-P {} '.format(num_processes)
Omar El Ayach07be7fb2020-09-01 13:50:46 -0700665 if socket_size:
Omar El Ayach6f8b62c2020-09-03 09:08:09 -0700666 iperf_args = iperf_args + '-w {} '.format(socket_size)
Omar El Ayach15effa32020-06-22 15:28:19 -0700667 if reverse_direction:
668 iperf_args = iperf_args + ' -R'
669 return iperf_args
670
671
Omar El Ayach2819be62019-04-11 12:00:00 -0700672# Attenuator Utilities
Omar El Ayach14416ac2019-01-30 14:58:19 -0800673def atten_by_label(atten_list, path_label, atten_level):
674 """Attenuate signals according to their path label.
675
676 Args:
677 atten_list: list of attenuators to iterate over
678 path_label: path label on which to set desired attenuation
679 atten_level: attenuation desired on path
680 """
681 for atten in atten_list:
682 if path_label in atten.path:
683 atten.set_atten(atten_level)
684
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700685
Omar El Ayach6d4b2b42020-04-02 14:49:03 -0700686def get_atten_for_target_rssi(target_rssi, attenuators, dut, ping_server):
687 """Function to estimate attenuation to hit a target RSSI.
688
689 This function estimates a constant attenuation setting on all atennuation
690 ports to hit a target RSSI. The estimate is not meant to be exact or
691 guaranteed.
692
693 Args:
694 target_rssi: rssi of interest
695 attenuators: list of attenuator ports
696 dut: android device object assumed connected to a wifi network.
697 ping_server: ssh connection object to ping server
698 Returns:
699 target_atten: attenuation setting to achieve target_rssi
700 """
701 logging.info('Searching attenuation for RSSI = {}dB'.format(target_rssi))
702 # Set attenuator to 0 dB
703 for atten in attenuators:
704 atten.set_atten(0, strict=False)
705 # Start ping traffic
706 dut_ip = dut.droid.connectivityGetIPv4Addresses('wlan0')[0]
707 # Measure starting RSSI
708 ping_future = get_ping_stats_nb(src_device=ping_server,
709 dest_address=dut_ip,
710 ping_duration=1.5,
711 ping_interval=0.02,
712 ping_size=64)
713 current_rssi = get_connected_rssi(dut,
714 num_measurements=4,
715 polling_frequency=0.25,
716 first_measurement_delay=0.5,
717 disconnect_warning=1,
718 ignore_samples=1)
719 current_rssi = current_rssi['signal_poll_rssi']['mean']
720 ping_future.result()
721 target_atten = 0
Omar El Ayachd4694892020-12-04 14:24:09 -0800722 logging.debug('RSSI @ {0:.2f}dB attenuation = {1:.2f}'.format(
Omar El Ayach6d4b2b42020-04-02 14:49:03 -0700723 target_atten, current_rssi))
724 within_range = 0
725 for idx in range(20):
726 atten_delta = max(min(current_rssi - target_rssi, 20), -20)
727 target_atten = int((target_atten + atten_delta) * 4) / 4
728 if target_atten < 0:
729 return 0
730 if target_atten > attenuators[0].get_max_atten():
731 return attenuators[0].get_max_atten()
732 for atten in attenuators:
733 atten.set_atten(target_atten, strict=False)
734 ping_future = get_ping_stats_nb(src_device=ping_server,
735 dest_address=dut_ip,
736 ping_duration=1.5,
737 ping_interval=0.02,
738 ping_size=64)
739 current_rssi = get_connected_rssi(dut,
740 num_measurements=4,
741 polling_frequency=0.25,
742 first_measurement_delay=0.5,
743 disconnect_warning=1,
744 ignore_samples=1)
745 current_rssi = current_rssi['signal_poll_rssi']['mean']
746 ping_future.result()
Omar El Ayachd4694892020-12-04 14:24:09 -0800747 logging.info('RSSI @ {0:.2f}dB attenuation = {1:.2f}'.format(
Omar El Ayach6d4b2b42020-04-02 14:49:03 -0700748 target_atten, current_rssi))
749 if abs(current_rssi - target_rssi) < 1:
750 if within_range:
751 logging.info(
752 'Reached RSSI: {0:.2f}. Target RSSI: {1:.2f}.'
753 'Attenuation: {2:.2f}, Iterations = {3:.2f}'.format(
754 current_rssi, target_rssi, target_atten, idx))
755 return target_atten
756 else:
757 within_range = True
758 else:
759 within_range = False
760 return target_atten
761
762
Omar El Ayach45ea3222020-12-21 22:00:49 -0800763def get_current_atten_dut_chain_map(attenuators,
764 dut,
765 ping_server,
766 ping_from_dut=False):
Omar El Ayach73477702019-08-07 10:12:33 -0700767 """Function to detect mapping between attenuator ports and DUT chains.
768
769 This function detects the mapping between attenuator ports and DUT chains
770 in cases where DUT chains are connected to only one attenuator port. The
771 function assumes the DUT is already connected to a wifi network. The
772 function starts by measuring per chain RSSI at 0 attenuation, then
773 attenuates one port at a time looking for the chain that reports a lower
774 RSSI.
775
776 Args:
777 attenuators: list of attenuator ports
778 dut: android device object assumed connected to a wifi network.
779 ping_server: ssh connection object to ping server
Omar El Ayach45ea3222020-12-21 22:00:49 -0800780 ping_from_dut: boolean controlling whether to ping from or to dut
Omar El Ayach73477702019-08-07 10:12:33 -0700781 Returns:
782 chain_map: list of dut chains, one entry per attenuator port
783 """
Omar El Ayach6e518a22019-06-13 13:55:42 -0700784 # Set attenuator to 0 dB
785 for atten in attenuators:
786 atten.set_atten(0, strict=False)
787 # Start ping traffic
Omar El Ayach73477702019-08-07 10:12:33 -0700788 dut_ip = dut.droid.connectivityGetIPv4Addresses('wlan0')[0]
Omar El Ayach45ea3222020-12-21 22:00:49 -0800789 if ping_from_dut:
790 ping_future = get_ping_stats_nb(dut, ping_server._settings.hostname,
791 11, 0.02, 64)
792 else:
793 ping_future = get_ping_stats_nb(ping_server, dut_ip, 11, 0.02, 64)
Omar El Ayach6e518a22019-06-13 13:55:42 -0700794 # Measure starting RSSI
795 base_rssi = get_connected_rssi(dut, 4, 0.25, 1)
796 chain0_base_rssi = base_rssi['chain_0_rssi']['mean']
797 chain1_base_rssi = base_rssi['chain_1_rssi']['mean']
Omar El Ayache9966592019-11-14 11:26:47 -0800798 if chain0_base_rssi < -70 or chain1_base_rssi < -70:
799 logging.warning('RSSI might be too low to get reliable chain map.')
Omar El Ayach6e518a22019-06-13 13:55:42 -0700800 # Compile chain map by attenuating one path at a time and seeing which
801 # chain's RSSI degrades
802 chain_map = []
803 for test_atten in attenuators:
Omar El Ayache9966592019-11-14 11:26:47 -0800804 # Set one attenuator to 30 dB down
Omar El Ayach73477702019-08-07 10:12:33 -0700805 test_atten.set_atten(30, strict=False)
Omar El Ayach6e518a22019-06-13 13:55:42 -0700806 # Get new RSSI
807 test_rssi = get_connected_rssi(dut, 4, 0.25, 1)
Omar El Ayach73477702019-08-07 10:12:33 -0700808 # Assign attenuator to path that has lower RSSI
Omar El Ayache9966592019-11-14 11:26:47 -0800809 if chain0_base_rssi > -70 and chain0_base_rssi - test_rssi[
810 'chain_0_rssi']['mean'] > 10:
Omar El Ayachbbc6a0a2019-07-22 10:26:11 -0700811 chain_map.append('DUT-Chain-0')
Omar El Ayache9966592019-11-14 11:26:47 -0800812 elif chain1_base_rssi > -70 and chain1_base_rssi - test_rssi[
813 'chain_1_rssi']['mean'] > 10:
Omar El Ayachbbc6a0a2019-07-22 10:26:11 -0700814 chain_map.append('DUT-Chain-1')
Omar El Ayach6e518a22019-06-13 13:55:42 -0700815 else:
816 chain_map.append(None)
Omar El Ayach73477702019-08-07 10:12:33 -0700817 # Reset attenuator to 0
818 test_atten.set_atten(0, strict=False)
Omar El Ayach6e518a22019-06-13 13:55:42 -0700819 ping_future.result()
Omar El Ayachdf470fb2019-09-16 12:22:28 -0700820 logging.debug('Chain Map: {}'.format(chain_map))
Omar El Ayach6e518a22019-06-13 13:55:42 -0700821 return chain_map
822
823
Omar El Ayach45ea3222020-12-21 22:00:49 -0800824def get_full_rf_connection_map(attenuators,
825 dut,
826 ping_server,
827 networks,
828 ping_from_dut=False):
Omar El Ayach73477702019-08-07 10:12:33 -0700829 """Function to detect per-network connections between attenuator and DUT.
830
831 This function detects the mapping between attenuator ports and DUT chains
832 on all networks in its arguments. The function connects the DUT to each
833 network then calls get_current_atten_dut_chain_map to get the connection
834 map on the current network. The function outputs the results in two formats
835 to enable easy access when users are interested in indexing by network or
836 attenuator port.
837
838 Args:
839 attenuators: list of attenuator ports
840 dut: android device object assumed connected to a wifi network.
841 ping_server: ssh connection object to ping server
842 networks: dict of network IDs and configs
843 Returns:
844 rf_map_by_network: dict of RF connections indexed by network.
845 rf_map_by_atten: list of RF connections indexed by attenuator
846 """
847 for atten in attenuators:
848 atten.set_atten(0, strict=False)
849
850 rf_map_by_network = collections.OrderedDict()
851 rf_map_by_atten = [[] for atten in attenuators]
852 for net_id, net_config in networks.items():
853 wutils.reset_wifi(dut)
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700854 wutils.wifi_connect(dut,
855 net_config,
856 num_of_tries=1,
857 assert_on_fail=False,
858 check_connectivity=False)
Omar El Ayach73477702019-08-07 10:12:33 -0700859 rf_map_by_network[net_id] = get_current_atten_dut_chain_map(
Omar El Ayach45ea3222020-12-21 22:00:49 -0800860 attenuators, dut, ping_server, ping_from_dut)
Omar El Ayach73477702019-08-07 10:12:33 -0700861 for idx, chain in enumerate(rf_map_by_network[net_id]):
862 if chain:
863 rf_map_by_atten[idx].append({
Omar El Ayachd4694892020-12-04 14:24:09 -0800864 'network': net_id,
865 'dut_chain': chain
Omar El Ayach73477702019-08-07 10:12:33 -0700866 })
Omar El Ayachd4694892020-12-04 14:24:09 -0800867 logging.debug('RF Map (by Network): {}'.format(rf_map_by_network))
868 logging.debug('RF Map (by Atten): {}'.format(rf_map_by_atten))
Omar El Ayachab047c02019-09-03 11:38:30 -0700869
Omar El Ayach73477702019-08-07 10:12:33 -0700870 return rf_map_by_network, rf_map_by_atten
871
Omar El Ayachbd8ddbd2019-10-16 23:33:07 -0700872
Omar El Ayach78213c52020-09-24 15:59:26 -0700873# Generic device utils
Omar El Ayachdb4fcd22019-09-04 11:55:37 -0700874def get_dut_temperature(dut):
875 """Function to get dut temperature.
876
877 The function fetches and returns the reading from the temperature sensor
878 used for skin temperature and thermal throttling.
879
880 Args:
881 dut: AndroidDevice of interest
882 Returns:
883 temperature: device temperature. 0 if temperature could not be read
884 """
Omar El Ayache5b8be12019-10-01 16:27:12 -0700885 candidate_zones = [
Omar El Ayach78075d62021-01-05 17:23:04 -0800886 '/sys/devices/virtual/thermal/tz-by-name/skin-therm/temp',
887 '/sys/devices/virtual/thermal/tz-by-name/sdm-therm-monitor/temp',
888 '/sys/devices/virtual/thermal/tz-by-name/sdm-therm-adc/temp',
889 '/sys/devices/virtual/thermal/tz-by-name/back_therm/temp',
890 '/dev/thermal/tz-by-name/quiet_therm/temp'
Omar El Ayache5b8be12019-10-01 16:27:12 -0700891 ]
Omar El Ayachdb4fcd22019-09-04 11:55:37 -0700892 for zone in candidate_zones:
893 try:
Omar El Ayach78075d62021-01-05 17:23:04 -0800894 temperature = int(dut.adb.shell('cat {}'.format(zone)))
Omar El Ayachdb4fcd22019-09-04 11:55:37 -0700895 break
Omar El Ayachc6a75752020-10-13 13:55:14 -0700896 except:
Omar El Ayachdb4fcd22019-09-04 11:55:37 -0700897 temperature = 0
898 if temperature == 0:
899 logging.debug('Could not check DUT temperature.')
900 elif temperature > 100:
901 temperature = temperature / 1000
902 return temperature
903
904
Omar El Ayach39493ff2019-11-18 16:15:12 -0800905def wait_for_dut_cooldown(dut, target_temp=50, timeout=300):
906 """Function to wait for a DUT to cool down.
907
908 Args:
909 dut: AndroidDevice of interest
910 target_temp: target cooldown temperature
911 timeout: maxt time to wait for cooldown
912 """
913 start_time = time.time()
914 while time.time() - start_time < timeout:
915 temperature = get_dut_temperature(dut)
916 if temperature < target_temp:
917 break
918 time.sleep(SHORT_SLEEP)
919 elapsed_time = time.time() - start_time
Omar El Ayachd4694892020-12-04 14:24:09 -0800920 logging.debug('DUT Final Temperature: {}C. Cooldown duration: {}'.format(
Omar El Ayach39493ff2019-11-18 16:15:12 -0800921 temperature, elapsed_time))
922
923
924def health_check(dut, batt_thresh=5, temp_threshold=53, cooldown=1):
Omar El Ayachdb4fcd22019-09-04 11:55:37 -0700925 """Function to check health status of a DUT.
926
927 The function checks both battery levels and temperature to avoid DUT
928 powering off during the test.
929
930 Args:
931 dut: AndroidDevice of interest
932 batt_thresh: battery level threshold
933 temp_threshold: temperature threshold
Omar El Ayach39493ff2019-11-18 16:15:12 -0800934 cooldown: flag to wait for DUT to cool down when overheating
Omar El Ayachdb4fcd22019-09-04 11:55:37 -0700935 Returns:
936 health_check: boolean confirming device is healthy
937 """
938 health_check = True
Omar El Ayach74f78dc2019-07-30 15:15:34 -0700939 battery_level = utils.get_battery_level(dut)
940 if battery_level < batt_thresh:
Omar El Ayachd4694892020-12-04 14:24:09 -0800941 logging.warning('Battery level low ({}%)'.format(battery_level))
Omar El Ayachdb4fcd22019-09-04 11:55:37 -0700942 health_check = False
Omar El Ayach40099d02019-09-12 15:17:33 -0700943 else:
Omar El Ayachd4694892020-12-04 14:24:09 -0800944 logging.debug('Battery level = {}%'.format(battery_level))
Omar El Ayachdb4fcd22019-09-04 11:55:37 -0700945
946 temperature = get_dut_temperature(dut)
947 if temperature > temp_threshold:
Omar El Ayach39493ff2019-11-18 16:15:12 -0800948 if cooldown:
949 logging.warning(
Omar El Ayachd4694892020-12-04 14:24:09 -0800950 'Waiting for DUT to cooldown. ({} C)'.format(temperature))
Omar El Ayach39493ff2019-11-18 16:15:12 -0800951 wait_for_dut_cooldown(dut, target_temp=temp_threshold - 5)
952 else:
Omar El Ayachd4694892020-12-04 14:24:09 -0800953 logging.warning('DUT Overheating ({} C)'.format(temperature))
Omar El Ayach39493ff2019-11-18 16:15:12 -0800954 health_check = False
Omar El Ayach40099d02019-09-12 15:17:33 -0700955 else:
Omar El Ayachd4694892020-12-04 14:24:09 -0800956 logging.debug('DUT Temperature = {} C'.format(temperature))
Omar El Ayachdb4fcd22019-09-04 11:55:37 -0700957 return health_check
Omar El Ayach4dc80ab2019-07-31 16:52:29 -0700958
959
Omar El Ayach8abe03b2020-09-24 16:16:54 -0700960# Wifi Device utils
961def detect_wifi_platform(dut):
962 ini_check = len(dut.get_file_names('/vendor/firmware/wlan/qca_cld/'))
963 if ini_check:
964 wifi_platform = 'qcom'
965 else:
966 wifi_platform = 'brcm'
967 return wifi_platform
968
969
970def detect_wifi_decorator(f):
971 def wrap(*args, **kwargs):
972 if 'dut' in kwargs:
973 dut = kwargs['dut']
974 else:
975 dut = next(arg for arg in args if type(arg) == AndroidDevice)
976 f_decorated = '{}_{}'.format(f.__name__, detect_wifi_platform(dut))
977 f_decorated = globals()[f_decorated]
978 return (f_decorated(*args, **kwargs))
979
980 return wrap
981
982
Omar El Ayach78213c52020-09-24 15:59:26 -0700983# Rssi Utilities
984def empty_rssi_result():
985 return collections.OrderedDict([('data', []), ('mean', None),
986 ('stdev', None)])
987
988
Omar El Ayach8abe03b2020-09-24 16:16:54 -0700989@detect_wifi_decorator
Omar El Ayach78213c52020-09-24 15:59:26 -0700990def get_connected_rssi(dut,
991 num_measurements=1,
992 polling_frequency=SHORT_SLEEP,
993 first_measurement_delay=0,
994 disconnect_warning=True,
995 ignore_samples=0,
996 interface=None):
997 """Gets all RSSI values reported for the connected access point/BSSID.
998
999 Args:
1000 dut: android device object from which to get RSSI
1001 num_measurements: number of scans done, and RSSIs collected
1002 polling_frequency: time to wait between RSSI measurements
1003 disconnect_warning: boolean controlling disconnection logging messages
1004 ignore_samples: number of leading samples to ignore
1005 Returns:
1006 connected_rssi: dict containing the measurements results for
1007 all reported RSSI values (signal_poll, per chain, etc.) and their
1008 statistics
1009 """
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001010 pass
1011
1012
1013@nonblocking
1014def get_connected_rssi_nb(dut,
1015 num_measurements=1,
1016 polling_frequency=SHORT_SLEEP,
1017 first_measurement_delay=0,
1018 disconnect_warning=True,
1019 ignore_samples=0,
1020 interface=None):
1021 return get_connected_rssi(dut, num_measurements, polling_frequency,
1022 first_measurement_delay, disconnect_warning,
1023 ignore_samples, interface)
1024
1025
1026def get_connected_rssi_qcom(dut,
1027 num_measurements=1,
1028 polling_frequency=SHORT_SLEEP,
1029 first_measurement_delay=0,
1030 disconnect_warning=True,
1031 ignore_samples=0,
1032 interface=None):
Omar El Ayach78213c52020-09-24 15:59:26 -07001033 # yapf: disable
1034 connected_rssi = collections.OrderedDict(
1035 [('time_stamp', []),
1036 ('bssid', []), ('ssid', []), ('frequency', []),
1037 ('signal_poll_rssi', empty_rssi_result()),
1038 ('signal_poll_avg_rssi', empty_rssi_result()),
1039 ('chain_0_rssi', empty_rssi_result()),
1040 ('chain_1_rssi', empty_rssi_result())])
1041 # yapf: enable
1042 previous_bssid = 'disconnected'
1043 t0 = time.time()
1044 time.sleep(first_measurement_delay)
1045 for idx in range(num_measurements):
1046 measurement_start_time = time.time()
1047 connected_rssi['time_stamp'].append(measurement_start_time - t0)
1048 # Get signal poll RSSI
Omar El Ayach5d37ed02020-11-23 13:23:51 -08001049 try:
1050 if interface is None:
1051 status_output = dut.adb.shell(WPA_CLI_STATUS)
1052 else:
1053 status_output = dut.adb.shell(
1054 'wpa_cli -i {} status'.format(interface))
1055 except:
1056 status_output = ''
Omar El Ayach78213c52020-09-24 15:59:26 -07001057 match = re.search('bssid=.*', status_output)
1058 if match:
1059 current_bssid = match.group(0).split('=')[1]
1060 connected_rssi['bssid'].append(current_bssid)
1061 else:
1062 current_bssid = 'disconnected'
1063 connected_rssi['bssid'].append(current_bssid)
1064 if disconnect_warning and previous_bssid != 'disconnected':
1065 logging.warning('WIFI DISCONNECT DETECTED!')
1066 previous_bssid = current_bssid
1067 match = re.search('\s+ssid=.*', status_output)
1068 if match:
1069 ssid = match.group(0).split('=')[1]
1070 connected_rssi['ssid'].append(ssid)
1071 else:
1072 connected_rssi['ssid'].append('disconnected')
Omar El Ayach5d37ed02020-11-23 13:23:51 -08001073 try:
1074 if interface is None:
1075 signal_poll_output = dut.adb.shell(SIGNAL_POLL)
1076 else:
1077 signal_poll_output = dut.adb.shell(
1078 'wpa_cli -i {} signal_poll'.format(interface))
1079 except:
1080 signal_poll_output = ''
Omar El Ayach78213c52020-09-24 15:59:26 -07001081 match = re.search('FREQUENCY=.*', signal_poll_output)
1082 if match:
1083 frequency = int(match.group(0).split('=')[1])
1084 connected_rssi['frequency'].append(frequency)
1085 else:
1086 connected_rssi['frequency'].append(RSSI_ERROR_VAL)
1087 match = re.search('RSSI=.*', signal_poll_output)
1088 if match:
1089 temp_rssi = int(match.group(0).split('=')[1])
1090 if temp_rssi == -9999 or temp_rssi == 0:
1091 connected_rssi['signal_poll_rssi']['data'].append(
1092 RSSI_ERROR_VAL)
1093 else:
1094 connected_rssi['signal_poll_rssi']['data'].append(temp_rssi)
1095 else:
1096 connected_rssi['signal_poll_rssi']['data'].append(RSSI_ERROR_VAL)
1097 match = re.search('AVG_RSSI=.*', signal_poll_output)
1098 if match:
1099 connected_rssi['signal_poll_avg_rssi']['data'].append(
1100 int(match.group(0).split('=')[1]))
1101 else:
1102 connected_rssi['signal_poll_avg_rssi']['data'].append(
1103 RSSI_ERROR_VAL)
1104
1105 # Get per chain RSSI
Omar El Ayach5d37ed02020-11-23 13:23:51 -08001106 try:
1107 if interface is None:
1108 per_chain_rssi = dut.adb.shell(STATION_DUMP)
1109 else:
1110 per_chain_rssi = ''
1111 except:
Omar El Ayach78213c52020-09-24 15:59:26 -07001112 per_chain_rssi = ''
1113 match = re.search('.*signal avg:.*', per_chain_rssi)
1114 if match:
1115 per_chain_rssi = per_chain_rssi[per_chain_rssi.find('[') +
1116 1:per_chain_rssi.find(']')]
1117 per_chain_rssi = per_chain_rssi.split(', ')
1118 connected_rssi['chain_0_rssi']['data'].append(
1119 int(per_chain_rssi[0]))
1120 connected_rssi['chain_1_rssi']['data'].append(
1121 int(per_chain_rssi[1]))
1122 else:
1123 connected_rssi['chain_0_rssi']['data'].append(RSSI_ERROR_VAL)
1124 connected_rssi['chain_1_rssi']['data'].append(RSSI_ERROR_VAL)
1125 measurement_elapsed_time = time.time() - measurement_start_time
1126 time.sleep(max(0, polling_frequency - measurement_elapsed_time))
1127
1128 # Compute mean RSSIs. Only average valid readings.
1129 # Output RSSI_ERROR_VAL if no valid connected readings found.
1130 for key, val in connected_rssi.copy().items():
1131 if 'data' not in val:
1132 continue
1133 filtered_rssi_values = [x for x in val['data'] if not math.isnan(x)]
1134 if len(filtered_rssi_values) > ignore_samples:
1135 filtered_rssi_values = filtered_rssi_values[ignore_samples:]
1136 if filtered_rssi_values:
1137 connected_rssi[key]['mean'] = statistics.mean(filtered_rssi_values)
1138 if len(filtered_rssi_values) > 1:
1139 connected_rssi[key]['stdev'] = statistics.stdev(
1140 filtered_rssi_values)
1141 else:
1142 connected_rssi[key]['stdev'] = 0
1143 else:
1144 connected_rssi[key]['mean'] = RSSI_ERROR_VAL
1145 connected_rssi[key]['stdev'] = RSSI_ERROR_VAL
1146 return connected_rssi
1147
1148
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001149def get_connected_rssi_brcm(dut,
1150 num_measurements=1,
1151 polling_frequency=SHORT_SLEEP,
1152 first_measurement_delay=0,
1153 disconnect_warning=True,
1154 ignore_samples=0,
1155 interface=None):
1156 # yapf: disable
1157 connected_rssi = collections.OrderedDict(
1158 [('time_stamp', []),
1159 ('bssid', []), ('ssid', []), ('frequency', []),
1160 ('signal_poll_rssi', empty_rssi_result()),
1161 ('signal_poll_avg_rssi', empty_rssi_result()),
1162 ('chain_0_rssi', empty_rssi_result()),
1163 ('chain_1_rssi', empty_rssi_result())])
1164
1165 # yapf: enable
1166 previous_bssid = 'disconnected'
1167 t0 = time.time()
1168 time.sleep(first_measurement_delay)
1169 for idx in range(num_measurements):
1170 measurement_start_time = time.time()
1171 connected_rssi['time_stamp'].append(measurement_start_time - t0)
1172 # Get signal poll RSSI
1173 status_output = dut.adb.shell('wl assoc')
1174 match = re.search('BSSID:.*', status_output)
1175
1176 if match:
1177 current_bssid = match.group(0).split('\t')[0]
1178 current_bssid = current_bssid.split(' ')[1]
1179 connected_rssi['bssid'].append(current_bssid)
1180
1181 else:
1182 current_bssid = 'disconnected'
1183 connected_rssi['bssid'].append(current_bssid)
1184 if disconnect_warning and previous_bssid != 'disconnected':
1185 logging.warning('WIFI DISCONNECT DETECTED!')
1186
1187 previous_bssid = current_bssid
1188 match = re.search('SSID:.*', status_output)
1189 if match:
1190 ssid = match.group(0).split(': ')[1]
1191 connected_rssi['ssid'].append(ssid)
1192 else:
1193 connected_rssi['ssid'].append('disconnected')
1194
1195 #TODO: SEARCH MAP ; PICK CENTER CHANNEL
1196 match = re.search('Primary channel:.*', status_output)
1197 if match:
1198 frequency = int(match.group(0).split(':')[1])
1199 connected_rssi['frequency'].append(frequency)
1200 else:
1201 connected_rssi['frequency'].append(RSSI_ERROR_VAL)
1202
Omar El Ayach8e40e5e2020-10-16 11:20:26 -07001203 try:
1204 per_chain_rssi = dut.adb.shell('wl phy_rssi_ant')
1205 except:
1206 per_chain_rssi = DISCONNECTION_MESSAGE_BRCM
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001207 if DISCONNECTION_MESSAGE_BRCM not in per_chain_rssi:
1208 per_chain_rssi = per_chain_rssi.split(' ')
Omar El Ayach229abd72020-11-02 17:25:23 -08001209 chain_0_rssi = int(per_chain_rssi[1])
1210 chain_1_rssi = int(per_chain_rssi[4])
1211 connected_rssi['chain_0_rssi']['data'].append(chain_0_rssi)
1212 connected_rssi['chain_1_rssi']['data'].append(chain_1_rssi)
1213 combined_rssi = math.pow(10, chain_0_rssi / 10) + math.pow(
1214 10, chain_1_rssi / 10)
1215 combined_rssi = 10 * math.log10(combined_rssi)
1216 connected_rssi['signal_poll_rssi']['data'].append(combined_rssi)
1217 connected_rssi['signal_poll_avg_rssi']['data'].append(
1218 combined_rssi)
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001219 else:
1220 connected_rssi['chain_0_rssi']['data'].append(RSSI_ERROR_VAL)
1221 connected_rssi['chain_1_rssi']['data'].append(RSSI_ERROR_VAL)
Omar El Ayach229abd72020-11-02 17:25:23 -08001222 connected_rssi['signal_poll_rssi']['data'].append(RSSI_ERROR_VAL)
1223 connected_rssi['signal_poll_avg_rssi']['data'].append(
1224 RSSI_ERROR_VAL)
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001225 measurement_elapsed_time = time.time() - measurement_start_time
1226 time.sleep(max(0, polling_frequency - measurement_elapsed_time))
1227
1228 # Statistics, Statistics
1229 for key, val in connected_rssi.copy().items():
1230 if 'data' not in val:
1231 continue
1232 filtered_rssi_values = [x for x in val['data'] if not math.isnan(x)]
1233 if len(filtered_rssi_values) > ignore_samples:
1234 filtered_rssi_values = filtered_rssi_values[ignore_samples:]
1235 if filtered_rssi_values:
1236 connected_rssi[key]['mean'] = statistics.mean(filtered_rssi_values)
1237 if len(filtered_rssi_values) > 1:
1238 connected_rssi[key]['stdev'] = statistics.stdev(
1239 filtered_rssi_values)
1240 else:
1241 connected_rssi[key]['stdev'] = 0
1242 else:
1243 connected_rssi[key]['mean'] = RSSI_ERROR_VAL
1244 connected_rssi[key]['stdev'] = RSSI_ERROR_VAL
1245
1246 return connected_rssi
Omar El Ayach78213c52020-09-24 15:59:26 -07001247
1248
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001249@detect_wifi_decorator
Omar El Ayach78213c52020-09-24 15:59:26 -07001250def get_scan_rssi(dut, tracked_bssids, num_measurements=1):
1251 """Gets scan RSSI for specified BSSIDs.
1252
1253 Args:
1254 dut: android device object from which to get RSSI
1255 tracked_bssids: array of BSSIDs to gather RSSI data for
1256 num_measurements: number of scans done, and RSSIs collected
1257 Returns:
1258 scan_rssi: dict containing the measurement results as well as the
1259 statistics of the scan RSSI for all BSSIDs in tracked_bssids
1260 """
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001261 pass
1262
1263
1264@nonblocking
1265def get_scan_rssi_nb(dut, tracked_bssids, num_measurements=1):
1266 return get_scan_rssi(dut, tracked_bssids, num_measurements)
1267
1268
1269def get_scan_rssi_qcom(dut, tracked_bssids, num_measurements=1):
Omar El Ayach78213c52020-09-24 15:59:26 -07001270 scan_rssi = collections.OrderedDict()
1271 for bssid in tracked_bssids:
1272 scan_rssi[bssid] = empty_rssi_result()
1273 for idx in range(num_measurements):
1274 scan_output = dut.adb.shell(SCAN)
1275 time.sleep(MED_SLEEP)
1276 scan_output = dut.adb.shell(SCAN_RESULTS)
1277 for bssid in tracked_bssids:
1278 bssid_result = re.search(bssid + '.*',
1279 scan_output,
1280 flags=re.IGNORECASE)
1281 if bssid_result:
1282 bssid_result = bssid_result.group(0).split('\t')
1283 scan_rssi[bssid]['data'].append(int(bssid_result[2]))
1284 else:
1285 scan_rssi[bssid]['data'].append(RSSI_ERROR_VAL)
1286 # Compute mean RSSIs. Only average valid readings.
1287 # Output RSSI_ERROR_VAL if no readings found.
1288 for key, val in scan_rssi.items():
1289 filtered_rssi_values = [x for x in val['data'] if not math.isnan(x)]
1290 if filtered_rssi_values:
1291 scan_rssi[key]['mean'] = statistics.mean(filtered_rssi_values)
1292 if len(filtered_rssi_values) > 1:
1293 scan_rssi[key]['stdev'] = statistics.stdev(
1294 filtered_rssi_values)
1295 else:
1296 scan_rssi[key]['stdev'] = 0
1297 else:
1298 scan_rssi[key]['mean'] = RSSI_ERROR_VAL
1299 scan_rssi[key]['stdev'] = RSSI_ERROR_VAL
1300 return scan_rssi
1301
1302
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001303def get_scan_rssi_brcm(dut, tracked_bssids, num_measurements=1):
1304 scan_rssi = collections.OrderedDict()
1305 for bssid in tracked_bssids:
1306 scan_rssi[bssid] = empty_rssi_result()
1307 for idx in range(num_measurements):
1308 scan_output = dut.adb.shell('cmd wifi start-scan')
1309 time.sleep(MED_SLEEP)
1310 scan_output = dut.adb.shell('cmd wifi list-scan-results')
1311 for bssid in tracked_bssids:
1312 bssid_result = re.search(bssid + '.*',
1313 scan_output,
1314 flags=re.IGNORECASE)
1315 if bssid_result:
1316 bssid_result = bssid_result.group(0).split()
1317 print(bssid_result)
1318 scan_rssi[bssid]['data'].append(int(bssid_result[2]))
1319 else:
1320 scan_rssi[bssid]['data'].append(RSSI_ERROR_VAL)
1321 # Compute mean RSSIs. Only average valid readings.
1322 # Output RSSI_ERROR_VAL if no readings found.
1323 for key, val in scan_rssi.items():
1324 filtered_rssi_values = [x for x in val['data'] if not math.isnan(x)]
1325 if filtered_rssi_values:
1326 scan_rssi[key]['mean'] = statistics.mean(filtered_rssi_values)
1327 if len(filtered_rssi_values) > 1:
1328 scan_rssi[key]['stdev'] = statistics.stdev(
1329 filtered_rssi_values)
1330 else:
1331 scan_rssi[key]['stdev'] = 0
1332 else:
1333 scan_rssi[key]['mean'] = RSSI_ERROR_VAL
1334 scan_rssi[key]['stdev'] = RSSI_ERROR_VAL
1335 return scan_rssi
1336
1337
1338@detect_wifi_decorator
Omar El Ayacha8c8d122020-03-24 12:48:06 -07001339def get_sw_signature(dut):
1340 """Function that checks the signature for wifi firmware and config files.
1341
1342 Returns:
1343 bdf_signature: signature consisting of last three digits of bdf cksums
1344 fw_signature: floating point firmware version, i.e., major.minor
1345 """
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001346 pass
1347
1348
1349def get_sw_signature_qcom(dut):
Omar El Ayacha8c8d122020-03-24 12:48:06 -07001350 bdf_output = dut.adb.shell('cksum /vendor/firmware/bdwlan*')
1351 logging.debug('BDF Checksum output: {}'.format(bdf_output))
1352 bdf_signature = sum(
1353 [int(line.split(' ')[0]) for line in bdf_output.splitlines()]) % 1000
1354
1355 fw_output = dut.adb.shell('halutil -logger -get fw')
1356 logging.debug('Firmware version output: {}'.format(fw_output))
1357 fw_version = re.search(FW_REGEX, fw_output).group('firmware')
1358 fw_signature = fw_version.split('.')[-3:-1]
1359 fw_signature = float('.'.join(fw_signature))
Omar El Ayach92442ad2020-05-13 11:37:19 -07001360 serial_hash = int(hashlib.md5(dut.serial.encode()).hexdigest(), 16) % 1000
1361 return {
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001362 'config_signature': bdf_signature,
Omar El Ayach92442ad2020-05-13 11:37:19 -07001363 'fw_signature': fw_signature,
1364 'serial_hash': serial_hash
1365 }
Omar El Ayacha8c8d122020-03-24 12:48:06 -07001366
1367
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001368def get_sw_signature_brcm(dut):
1369 bdf_output = dut.adb.shell('cksum /vendor/etc/wifi/bcmdhd*')
1370 logging.debug('BDF Checksum output: {}'.format(bdf_output))
1371 bdf_signature = sum(
1372 [int(line.split(' ')[0]) for line in bdf_output.splitlines()]) % 1000
1373
1374 fw_output = dut.adb.shell('getprop vendor.wlan.firmware.version')
1375 logging.debug('Firmware version output: {}'.format(fw_output))
1376 fw_version = fw_output.split('.')[-1]
1377 driver_output = dut.adb.shell('getprop vendor.wlan.driver.version')
1378 driver_version = driver_output.split('.')[-1]
1379 fw_signature = float('{}.{}'.format(fw_version, driver_version))
1380 serial_hash = int(hashlib.md5(dut.serial.encode()).hexdigest(), 16) % 1000
1381 return {
1382 'config_signature': bdf_signature,
1383 'fw_signature': fw_signature,
1384 'serial_hash': serial_hash
1385 }
1386
1387
1388@detect_wifi_decorator
1389def push_config(dut, config_file):
Omar El Ayach4dc80ab2019-07-31 16:52:29 -07001390 """Function to push Wifi BDF files
1391
1392 This function checks for existing wifi bdf files and over writes them all,
1393 for simplicity, with the bdf file provided in the arguments. The dut is
1394 rebooted for the bdf file to take effect
1395
1396 Args:
1397 dut: dut to push bdf file to
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001398 config_file: path to bdf_file to push
Omar El Ayach4dc80ab2019-07-31 16:52:29 -07001399 """
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001400 pass
1401
1402
1403def push_config_qcom(dut, config_file):
1404 config_files_list = dut.adb.shell(
1405 'ls /vendor/firmware/bdwlan*').splitlines()
1406 for dst_file in config_files_list:
1407 dut.push_system_file(config_file, dst_file)
Omar El Ayach4dc80ab2019-07-31 16:52:29 -07001408 dut.reboot()
1409
1410
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001411def push_config_brcm(dut, config_file):
1412 config_files_list = dut.adb.shell('ls /vendor/etc/*.cal').splitlines()
1413 for dst_file in config_files_list:
1414 dut.push_system_file(config_file, dst_file)
1415 dut.reboot()
1416
1417
1418def push_firmware(dut, firmware_files):
Omar El Ayach4dc80ab2019-07-31 16:52:29 -07001419 """Function to push Wifi firmware files
1420
1421 Args:
1422 dut: dut to push bdf file to
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001423 firmware_files: path to wlanmdsp.mbn file
Omar El Ayach4dc80ab2019-07-31 16:52:29 -07001424 datamsc_file: path to Data.msc file
1425 """
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001426 for file in firmware_files:
1427 dut.push_system_file(file, '/vendor/firmware/')
Omar El Ayach73477702019-08-07 10:12:33 -07001428 dut.reboot()
Omar El Ayache5b8be12019-10-01 16:27:12 -07001429
1430
Omar El Ayachc2dd2232021-01-13 14:32:55 -08001431@detect_wifi_decorator
1432def start_wifi_logging(dut):
1433 """Function to start collecting wifi-related logs"""
1434 pass
1435
1436
1437def start_wifi_logging_qcom(dut):
1438 dut.droid.wifiEnableVerboseLogging(1)
1439 msg = "Failed to enable WiFi verbose logging."
1440 asserts.assert_equal(dut.droid.wifiGetVerboseLoggingLevel(), 1, msg)
1441 logging.info('Starting CNSS logs')
1442 dut.adb.shell("find /data/vendor/wifi/wlan_logs/ -type f -delete",
1443 ignore_status=True)
1444 dut.adb.shell_nb('cnss_diag -f -s')
1445
1446
1447def start_wifi_logging_brcm(dut):
1448 pass
1449
1450
1451@detect_wifi_decorator
1452def stop_wifi_logging(dut):
1453 """Function to start collecting wifi-related logs"""
1454 pass
1455
1456
1457def stop_wifi_logging_qcom(dut):
1458 logging.info('Stopping CNSS logs')
1459 dut.adb.shell('killall cnss_diag')
1460 logs = dut.get_file_names("/data/vendor/wifi/wlan_logs/")
1461 if logs:
1462 dut.log.info("Pulling cnss_diag logs %s", logs)
1463 log_path = os.path.join(dut.device_log_path,
1464 "CNSS_DIAG_%s" % dut.serial)
1465 os.makedirs(log_path, exist_ok=True)
1466 dut.pull_files(logs, log_path)
1467
1468
1469def stop_wifi_logging_brcm(dut):
1470 pass
1471
1472
Omar El Ayache5b8be12019-10-01 16:27:12 -07001473def _set_ini_fields(ini_file_path, ini_field_dict):
1474 template_regex = r'^{}=[0-9,.x-]+'
1475 with open(ini_file_path, 'r') as f:
1476 ini_lines = f.read().splitlines()
1477 for idx, line in enumerate(ini_lines):
1478 for field_name, field_value in ini_field_dict.items():
1479 line_regex = re.compile(template_regex.format(field_name))
1480 if re.match(line_regex, line):
Omar El Ayachd4694892020-12-04 14:24:09 -08001481 ini_lines[idx] = '{}={}'.format(field_name, field_value)
Omar El Ayache5b8be12019-10-01 16:27:12 -07001482 print(ini_lines[idx])
1483 with open(ini_file_path, 'w') as f:
Omar El Ayachd4694892020-12-04 14:24:09 -08001484 f.write('\n'.join(ini_lines) + '\n')
Omar El Ayache5b8be12019-10-01 16:27:12 -07001485
Omar El Ayach0d4358e2020-02-06 15:30:08 -08001486
Omar El Ayache5b8be12019-10-01 16:27:12 -07001487def _edit_dut_ini(dut, ini_fields):
1488 """Function to edit Wifi ini files."""
1489 dut_ini_path = '/vendor/firmware/wlan/qca_cld/WCNSS_qcom_cfg.ini'
1490 local_ini_path = os.path.expanduser('~/WCNSS_qcom_cfg.ini')
1491 dut.pull_files(dut_ini_path, local_ini_path)
1492
1493 _set_ini_fields(local_ini_path, ini_fields)
1494
1495 dut.push_system_file(local_ini_path, dut_ini_path)
1496 dut.reboot()
1497
1498
1499def set_ini_single_chain_mode(dut, chain):
1500 ini_fields = {
1501 'gEnable2x2': 0,
Omar El Ayach0d4358e2020-02-06 15:30:08 -08001502 'gSetTxChainmask1x1': chain + 1,
1503 'gSetRxChainmask1x1': chain + 1,
Omar El Ayache5b8be12019-10-01 16:27:12 -07001504 'gDualMacFeatureDisable': 1,
1505 'gDot11Mode': 0
1506 }
1507 _edit_dut_ini(dut, ini_fields)
1508
1509
1510def set_ini_two_chain_mode(dut):
1511 ini_fields = {
1512 'gEnable2x2': 2,
1513 'gSetTxChainmask1x1': 1,
1514 'gSetRxChainmask1x1': 1,
1515 'gDualMacFeatureDisable': 6,
1516 'gDot11Mode': 0
1517 }
1518 _edit_dut_ini(dut, ini_fields)
1519
1520
1521def set_ini_tx_mode(dut, mode):
1522 TX_MODE_DICT = {
Omar El Ayachd4694892020-12-04 14:24:09 -08001523 'Auto': 0,
1524 '11n': 4,
1525 '11ac': 9,
1526 '11abg': 1,
1527 '11b': 2,
1528 '11': 3,
1529 '11g only': 5,
1530 '11n only': 6,
1531 '11b only': 7,
1532 '11ac only': 8
Omar El Ayache5b8be12019-10-01 16:27:12 -07001533 }
1534
1535 ini_fields = {
1536 'gEnable2x2': 2,
1537 'gSetTxChainmask1x1': 1,
1538 'gSetRxChainmask1x1': 1,
1539 'gDualMacFeatureDisable': 6,
1540 'gDot11Mode': TX_MODE_DICT[mode]
1541 }
1542 _edit_dut_ini(dut, ini_fields)
Omar El Ayach78213c52020-09-24 15:59:26 -07001543
1544
1545# Link layer stats utilities
1546class LinkLayerStats():
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001547 def __new__(self, dut, llstats_enabled=True):
1548 if detect_wifi_platform(dut) == 'qcom':
1549 return LinkLayerStatsQcom(dut, llstats_enabled=True)
1550 else:
1551 return LinkLayerStatsBrcm(dut, llstats_enabled=True)
1552
1553
1554class LinkLayerStatsQcom():
Omar El Ayach78213c52020-09-24 15:59:26 -07001555
1556 LLSTATS_CMD = 'cat /d/wlan0/ll_stats'
1557 PEER_REGEX = 'LL_STATS_PEER_ALL'
1558 MCS_REGEX = re.compile(
1559 r'preamble: (?P<mode>\S+), nss: (?P<num_streams>\S+), bw: (?P<bw>\S+), '
1560 'mcs: (?P<mcs>\S+), bitrate: (?P<rate>\S+), txmpdu: (?P<txmpdu>\S+), '
1561 'rxmpdu: (?P<rxmpdu>\S+), mpdu_lost: (?P<mpdu_lost>\S+), '
1562 'retries: (?P<retries>\S+), retries_short: (?P<retries_short>\S+), '
1563 'retries_long: (?P<retries_long>\S+)')
1564 MCS_ID = collections.namedtuple(
1565 'mcs_id', ['mode', 'num_streams', 'bandwidth', 'mcs', 'rate'])
1566 MODE_MAP = {'0': '11a/g', '1': '11b', '2': '11n', '3': '11ac'}
1567 BW_MAP = {'0': 20, '1': 40, '2': 80}
1568
1569 def __init__(self, dut, llstats_enabled=True):
1570 self.dut = dut
1571 self.llstats_enabled = llstats_enabled
1572 self.llstats_cumulative = self._empty_llstats()
1573 self.llstats_incremental = self._empty_llstats()
1574
1575 def update_stats(self):
1576 if self.llstats_enabled:
1577 try:
1578 llstats_output = self.dut.adb.shell(self.LLSTATS_CMD,
1579 timeout=0.1)
1580 except:
1581 llstats_output = ''
1582 else:
1583 llstats_output = ''
1584 self._update_stats(llstats_output)
1585
1586 def reset_stats(self):
1587 self.llstats_cumulative = self._empty_llstats()
1588 self.llstats_incremental = self._empty_llstats()
1589
1590 def _empty_llstats(self):
1591 return collections.OrderedDict(mcs_stats=collections.OrderedDict(),
1592 summary=collections.OrderedDict())
1593
1594 def _empty_mcs_stat(self):
1595 return collections.OrderedDict(txmpdu=0,
1596 rxmpdu=0,
1597 mpdu_lost=0,
1598 retries=0,
1599 retries_short=0,
1600 retries_long=0)
1601
1602 def _mcs_id_to_string(self, mcs_id):
1603 mcs_string = '{} {}MHz Nss{} MCS{} {}Mbps'.format(
1604 mcs_id.mode, mcs_id.bandwidth, mcs_id.num_streams, mcs_id.mcs,
1605 mcs_id.rate)
1606 return mcs_string
1607
1608 def _parse_mcs_stats(self, llstats_output):
1609 llstats_dict = {}
1610 # Look for per-peer stats
1611 match = re.search(self.PEER_REGEX, llstats_output)
1612 if not match:
1613 self.reset_stats()
1614 return collections.OrderedDict()
1615 # Find and process all matches for per stream stats
1616 match_iter = re.finditer(self.MCS_REGEX, llstats_output)
1617 for match in match_iter:
1618 current_mcs = self.MCS_ID(self.MODE_MAP[match.group('mode')],
1619 int(match.group('num_streams')) + 1,
1620 self.BW_MAP[match.group('bw')],
1621 int(match.group('mcs')),
1622 int(match.group('rate'), 16) / 1000)
1623 current_stats = collections.OrderedDict(
1624 txmpdu=int(match.group('txmpdu')),
1625 rxmpdu=int(match.group('rxmpdu')),
1626 mpdu_lost=int(match.group('mpdu_lost')),
1627 retries=int(match.group('retries')),
1628 retries_short=int(match.group('retries_short')),
1629 retries_long=int(match.group('retries_long')))
1630 llstats_dict[self._mcs_id_to_string(current_mcs)] = current_stats
1631 return llstats_dict
1632
1633 def _diff_mcs_stats(self, new_stats, old_stats):
1634 stats_diff = collections.OrderedDict()
1635 for stat_key in new_stats.keys():
1636 stats_diff[stat_key] = new_stats[stat_key] - old_stats[stat_key]
1637 return stats_diff
1638
1639 def _generate_stats_summary(self, llstats_dict):
1640 llstats_summary = collections.OrderedDict(common_tx_mcs=None,
1641 common_tx_mcs_count=0,
1642 common_tx_mcs_freq=0,
1643 common_rx_mcs=None,
1644 common_rx_mcs_count=0,
1645 common_rx_mcs_freq=0)
1646 txmpdu_count = 0
1647 rxmpdu_count = 0
1648 for mcs_id, mcs_stats in llstats_dict['mcs_stats'].items():
1649 if mcs_stats['txmpdu'] > llstats_summary['common_tx_mcs_count']:
1650 llstats_summary['common_tx_mcs'] = mcs_id
1651 llstats_summary['common_tx_mcs_count'] = mcs_stats['txmpdu']
1652 if mcs_stats['rxmpdu'] > llstats_summary['common_rx_mcs_count']:
1653 llstats_summary['common_rx_mcs'] = mcs_id
1654 llstats_summary['common_rx_mcs_count'] = mcs_stats['rxmpdu']
1655 txmpdu_count += mcs_stats['txmpdu']
1656 rxmpdu_count += mcs_stats['rxmpdu']
1657 if txmpdu_count:
1658 llstats_summary['common_tx_mcs_freq'] = (
1659 llstats_summary['common_tx_mcs_count'] / txmpdu_count)
1660 if rxmpdu_count:
1661 llstats_summary['common_rx_mcs_freq'] = (
1662 llstats_summary['common_rx_mcs_count'] / rxmpdu_count)
1663 return llstats_summary
1664
1665 def _update_stats(self, llstats_output):
1666 # Parse stats
1667 new_llstats = self._empty_llstats()
1668 new_llstats['mcs_stats'] = self._parse_mcs_stats(llstats_output)
1669 # Save old stats and set new cumulative stats
1670 old_llstats = self.llstats_cumulative.copy()
1671 self.llstats_cumulative = new_llstats.copy()
1672 # Compute difference between new and old stats
1673 self.llstats_incremental = self._empty_llstats()
1674 for mcs_id, new_mcs_stats in new_llstats['mcs_stats'].items():
1675 old_mcs_stats = old_llstats['mcs_stats'].get(
1676 mcs_id, self._empty_mcs_stat())
1677 self.llstats_incremental['mcs_stats'][
1678 mcs_id] = self._diff_mcs_stats(new_mcs_stats, old_mcs_stats)
1679 # Generate llstats summary
1680 self.llstats_incremental['summary'] = self._generate_stats_summary(
1681 self.llstats_incremental)
1682 self.llstats_cumulative['summary'] = self._generate_stats_summary(
1683 self.llstats_cumulative)
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001684
1685
1686class LinkLayerStatsBrcm():
1687 def __init__(self, dut, llstats_enabled=True):
1688 self.dut = dut
1689 self.llstats_enabled = llstats_enabled
1690 self.llstats_incremental = self._empty_llstats()
1691 self.llstats_cumulative = self.llstats_incremental
1692
1693 def _empty_llstats(self):
1694 return collections.OrderedDict(mcs_stats=collections.OrderedDict(),
1695 summary=collections.OrderedDict())
1696
1697 def update_stats(self):
1698 self.llstats_incremental = self._empty_llstats()
1699 self.llstats_incremental['summary'] = collections.OrderedDict(
1700 common_tx_mcs=None,
1701 common_tx_mcs_count=1,
1702 common_tx_mcs_freq=1,
1703 common_rx_mcs=None,
1704 common_rx_mcs_count=1,
1705 common_rx_mcs_freq=1)
1706 if self.llstats_enabled:
1707 try:
1708 rate_info = self.dut.adb.shell('wl rate_info', timeout=0.1)
1709 self.llstats_incremental['summary'][
Omar El Ayachd4694892020-12-04 14:24:09 -08001710 'common_tx_mcs'] = '{} Mbps'.format(
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001711 re.findall('\[Tx\]:'
1712 ' (\d+[.]*\d* Mbps)', rate_info))
1713 self.llstats_incremental['summary'][
Omar El Ayachd4694892020-12-04 14:24:09 -08001714 'common_rx_mcs'] = '{} Mbps'.format(
Omar El Ayach8abe03b2020-09-24 16:16:54 -07001715 re.findall('\[Rx\]:'
1716 ' (\d+[.]*\d* Mbps)', rate_info))
1717 except:
1718 pass