blob: afa5f328f40831561d93c7830b1e13ceb2a90123 [file] [log] [blame]
#!/usr/bin/env python3.4
#
# Copyright 2021 - The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import hashlib
import itertools
import logging
import math
import numpy
import re
import statistics
import time
VERY_SHORT_SLEEP = 0.5
SHORT_SLEEP = 1
MED_SLEEP = 6
DISCONNECTION_MESSAGE_BRCM = 'driver adapter not found'
RSSI_ERROR_VAL = float('nan')
RATE_TABLE = {
'HT': {
1: {
20: [7.2, 14.4, 21.7, 28.9, 43.4, 57.8, 65.0, 72.2],
40: [15.0, 30.0, 45.0, 60.0, 90.0, 120.0, 135.0, 150.0]
},
2: {
20: [
0, 0, 0, 0, 0, 0, 0, 0, 14.4, 28.8, 43.4, 57.8, 86.8, 115.6,
130, 144.4
],
40: [0, 0, 0, 0, 0, 0, 0, 0, 30, 60, 90, 120, 180, 240, 270, 300]
}
},
'VHT': {
1: {
20: [
7.2, 14.4, 21.7, 28.9, 43.4, 57.8, 65.0, 72.2, 86.7, 96.2,
129.0, 143.4
],
40: [
15.0, 30.0, 45.0, 60.0, 90.0, 120.0, 135.0, 150.0, 180.0,
200.0, 258, 286.8
],
80: [
32.5, 65.0, 97.5, 130.0, 195.0, 260.0, 292.5, 325.0, 390.0,
433.3, 540.4, 600.4
],
160: [
65.0, 130.0, 195.0, 260.0, 390.0, 520.0, 585.0, 650.0, 780.0,
1080.8, 1200.8
]
},
2: {
20: [
14.4, 28.8, 43.4, 57.8, 86.8, 115.6, 130, 144.4, 173.4, 192.4,
258, 286.8
],
40: [30, 60, 90, 120, 180, 240, 270, 300, 360, 400, 516, 573.6],
80: [
65, 130, 195, 260, 390, 520, 585, 650, 780, 866.6, 1080.8,
1200.8
],
160:
[130, 260, 390, 520, 780, 1040, 1170, 1300, 1560, 2161.6, 2401.6]
},
},
'HE': {
1: {
20: [
8.6, 17.2, 25.8, 34.4, 51.6, 68.8, 77.4, 86.0, 103.2, 114.7,
129.0, 143.4
],
40: [
17.2, 34.4, 51.6, 68.8, 103.2, 137.6, 154.8, 172, 206.4, 229.4,
258, 286.8
],
80: [
36.0, 72.1, 108.1, 144.1, 216.2, 288.2, 324.3, 360.3, 432.4,
480.4, 540.4, 600.4
],
160: [
72, 144.2, 216.2, 288.2, 432.4, 576.4, 648.6, 720.6, 864.8,
960.8, 1080.8, 1200.8
]
},
2: {
20: [
17.2, 34.4, 51.6, 68.8, 103.2, 137.6, 154.8, 172, 206.4, 229.4,
258, 286.8
],
40: [
34.4, 68.8, 103.2, 137.6, 206.4, 275.2, 309.6, 344, 412.8,
458.8, 516, 573.6
],
80: [
72, 144.2, 216.2, 288.2, 432.4, 576.4, 648.6, 720.6, 864.8,
960.8, 1080.8, 1200.8
],
160: [
144, 288.4, 432.4, 576.4, 864.8, 1152.8, 1297.2, 1441.2,
1729.6, 1921.6, 2161.6, 2401.6
]
},
},
}
# Rssi Utilities
def empty_rssi_result():
return collections.OrderedDict([('data', []), ('mean', None),
('stdev', None)])
def get_connected_rssi(dut,
num_measurements=1,
polling_frequency=SHORT_SLEEP,
first_measurement_delay=0,
disconnect_warning=True,
ignore_samples=0,
interface='wlan0'):
# yapf: disable
connected_rssi = collections.OrderedDict(
[('time_stamp', []),
('bssid', []), ('ssid', []), ('frequency', []),
('signal_poll_rssi', empty_rssi_result()),
('signal_poll_avg_rssi', empty_rssi_result()),
('chain_0_rssi', empty_rssi_result()),
('chain_1_rssi', empty_rssi_result())])
# yapf: enable
previous_bssid = 'disconnected'
t0 = time.time()
time.sleep(first_measurement_delay)
for idx in range(num_measurements):
measurement_start_time = time.time()
connected_rssi['time_stamp'].append(measurement_start_time - t0)
# Get signal poll RSSI
try:
status_output = dut.adb.shell(
'wpa_cli -i {} status'.format(interface))
except:
status_output = ''
match = re.search('bssid=.*', status_output)
if match:
current_bssid = match.group(0).split('=')[1]
connected_rssi['bssid'].append(current_bssid)
else:
current_bssid = 'disconnected'
connected_rssi['bssid'].append(current_bssid)
if disconnect_warning and previous_bssid != 'disconnected':
logging.warning('WIFI DISCONNECT DETECTED!')
previous_bssid = current_bssid
match = re.search('\s+ssid=.*', status_output)
if match:
ssid = match.group(0).split('=')[1]
connected_rssi['ssid'].append(ssid)
else:
connected_rssi['ssid'].append('disconnected')
#TODO: SEARCH MAP ; PICK CENTER CHANNEL
match = re.search('\s+freq=.*', status_output)
if match:
frequency = int(match.group(0).split('=')[1])
connected_rssi['frequency'].append(frequency)
else:
connected_rssi['frequency'].append(RSSI_ERROR_VAL)
if interface == 'wlan0':
try:
per_chain_rssi = dut.adb.shell('wl phy_rssi_ant')
chain_0_rssi = re.search(
r'rssi\[0\]\s(?P<chain_0_rssi>[0-9\-]*)', per_chain_rssi)
if chain_0_rssi:
chain_0_rssi = int(chain_0_rssi.group('chain_0_rssi'))
else:
chain_0_rssi = -float('inf')
chain_1_rssi = re.search(
r'rssi\[1\]\s(?P<chain_1_rssi>[0-9\-]*)', per_chain_rssi)
if chain_1_rssi:
chain_1_rssi = int(chain_1_rssi.group('chain_1_rssi'))
else:
chain_1_rssi = -float('inf')
except:
chain_0_rssi = RSSI_ERROR_VAL
chain_1_rssi = RSSI_ERROR_VAL
connected_rssi['chain_0_rssi']['data'].append(chain_0_rssi)
connected_rssi['chain_1_rssi']['data'].append(chain_1_rssi)
combined_rssi = math.pow(10, chain_0_rssi / 10) + math.pow(
10, chain_1_rssi / 10)
combined_rssi = 10 * math.log10(combined_rssi)
connected_rssi['signal_poll_rssi']['data'].append(combined_rssi)
connected_rssi['signal_poll_avg_rssi']['data'].append(
combined_rssi)
else:
try:
signal_poll_output = dut.adb.shell(
'wpa_cli -i {} signal_poll'.format(interface))
except:
signal_poll_output = ''
match = re.search('RSSI=.*', signal_poll_output)
if match:
temp_rssi = int(match.group(0).split('=')[1])
if temp_rssi == -9999 or temp_rssi == 0:
connected_rssi['signal_poll_rssi']['data'].append(
RSSI_ERROR_VAL)
else:
connected_rssi['signal_poll_rssi']['data'].append(
temp_rssi)
else:
connected_rssi['signal_poll_rssi']['data'].append(
RSSI_ERROR_VAL)
connected_rssi['chain_0_rssi']['data'].append(RSSI_ERROR_VAL)
connected_rssi['chain_1_rssi']['data'].append(RSSI_ERROR_VAL)
measurement_elapsed_time = time.time() - measurement_start_time
time.sleep(max(0, polling_frequency - measurement_elapsed_time))
# Statistics, Statistics
for key, val in connected_rssi.copy().items():
if 'data' not in val:
continue
filtered_rssi_values = [x for x in val['data'] if not math.isnan(x)]
if len(filtered_rssi_values) > ignore_samples:
filtered_rssi_values = filtered_rssi_values[ignore_samples:]
if filtered_rssi_values:
connected_rssi[key]['mean'] = statistics.mean(filtered_rssi_values)
if len(filtered_rssi_values) > 1:
connected_rssi[key]['stdev'] = statistics.stdev(
filtered_rssi_values)
else:
connected_rssi[key]['stdev'] = 0
else:
connected_rssi[key]['mean'] = RSSI_ERROR_VAL
connected_rssi[key]['stdev'] = RSSI_ERROR_VAL
return connected_rssi
def get_scan_rssi(dut, tracked_bssids, num_measurements=1):
scan_rssi = collections.OrderedDict()
for bssid in tracked_bssids:
scan_rssi[bssid] = empty_rssi_result()
for idx in range(num_measurements):
scan_output = dut.adb.shell('cmd wifi start-scan')
time.sleep(MED_SLEEP)
scan_output = dut.adb.shell('cmd wifi list-scan-results')
for bssid in tracked_bssids:
bssid_result = re.search(bssid + '.*',
scan_output,
flags=re.IGNORECASE)
if bssid_result:
bssid_result = bssid_result.group(0).split()
scan_rssi[bssid]['data'].append(int(bssid_result[2]))
else:
scan_rssi[bssid]['data'].append(RSSI_ERROR_VAL)
# Compute mean RSSIs. Only average valid readings.
# Output RSSI_ERROR_VAL if no readings found.
for key, val in scan_rssi.items():
filtered_rssi_values = [x for x in val['data'] if not math.isnan(x)]
if filtered_rssi_values:
scan_rssi[key]['mean'] = statistics.mean(filtered_rssi_values)
if len(filtered_rssi_values) > 1:
scan_rssi[key]['stdev'] = statistics.stdev(
filtered_rssi_values)
else:
scan_rssi[key]['stdev'] = 0
else:
scan_rssi[key]['mean'] = RSSI_ERROR_VAL
scan_rssi[key]['stdev'] = RSSI_ERROR_VAL
return scan_rssi
def get_sw_signature(dut):
bdf_output = dut.adb.shell('cksum /vendor/firmware/bcmdhd*')
logging.debug('BDF Checksum output: {}'.format(bdf_output))
bdf_signature = sum(
[int(line.split(' ')[0]) for line in bdf_output.splitlines()]) % 1000
fw_version = dut.adb.shell('getprop vendor.wlan.firmware.version')
driver_version = dut.adb.shell('getprop vendor.wlan.driver.version')
logging.debug('Firmware version : {}. Driver version: {}'.format(
fw_version, driver_version))
fw_signature = '{}+{}'.format(fw_version, driver_version)
fw_signature = int(hashlib.md5(fw_signature.encode()).hexdigest(),
16) % 1000
serial_hash = int(hashlib.md5(dut.serial.encode()).hexdigest(), 16) % 1000
return {
'config_signature': bdf_signature,
'fw_signature': fw_signature,
'serial_hash': serial_hash
}
def get_country_code(dut):
try:
country_code = dut.adb.shell('wl country').split(' ')[0]
except:
country_code = 'XZ'
if country_code == 'XZ':
country_code = 'WW'
logging.debug('Country code: {}'.format(country_code))
return country_code
def push_config(dut, config_file):
config_files_list = dut.adb.shell('ls /vendor/etc/*.cal').splitlines()
for dst_file in config_files_list:
dut.push_system_file(config_file, dst_file)
dut.reboot()
def start_wifi_logging(dut):
pass
def stop_wifi_logging(dut):
pass
def push_firmware(dut, firmware_files):
"""Function to push Wifi firmware files
Args:
dut: dut to push bdf file to
firmware_files: path to wlanmdsp.mbn file
datamsc_file: path to Data.msc file
"""
for file in firmware_files:
dut.push_system_file(file, '/vendor/firmware/')
dut.reboot()
def disable_beamforming(dut):
dut.adb.shell('wl txbf 0')
def set_nss_capability(dut, nss):
dut.adb.shell('wl he omi -r {} -t {}'.format(nss, nss))
def set_chain_mask(dut, chain):
if chain == '2x2':
chain = 3
else:
chain = chain + 1
# Get current chain mask
try:
curr_tx_chain = int(dut.adb.shell('wl txchain'))
curr_rx_chain = int(dut.adb.shell('wl rxchain'))
except:
curr_tx_chain = -1
curr_rx_chain = -1
if curr_tx_chain == chain and curr_rx_chain == chain:
return
# Set chain mask if needed
dut.adb.shell('wl down')
time.sleep(VERY_SHORT_SLEEP)
dut.adb.shell('wl txchain 0x{}'.format(chain))
dut.adb.shell('wl rxchain 0x{}'.format(chain))
dut.adb.shell('wl up')
class LinkLayerStats():
LLSTATS_CMD = 'wl dump ampdu; wl counters;'
LL_STATS_CLEAR_CMD = 'wl dump_clear ampdu; wl reset_cnts;'
BW_REGEX = re.compile(r'Chanspec:.+ (?P<bandwidth>[0-9]+)MHz')
MCS_REGEX = re.compile(r'(?P<count>[0-9]+)\((?P<percent>[0-9]+)%\)')
RX_REGEX = re.compile(r'RX (?P<mode>\S+)\s+:\s*(?P<nss1>[0-9, ,(,),%]*)'
'\n\s*:?\s*(?P<nss2>[0-9, ,(,),%]*)')
TX_REGEX = re.compile(r'TX (?P<mode>\S+)\s+:\s*(?P<nss1>[0-9, ,(,),%]*)'
'\n\s*:?\s*(?P<nss2>[0-9, ,(,),%]*)')
TX_PER_REGEX = re.compile(
r'(?P<mode>\S+) PER\s+:\s*(?P<nss1>[0-9, ,(,),%]*)'
'\n\s*:?\s*(?P<nss2>[0-9, ,(,),%]*)')
RX_FCS_REGEX = re.compile(
r'rxbadfcs (?P<rx_bad_fcs>[0-9]*).+\n.+goodfcs (?P<rx_good_fcs>[0-9]*)'
)
RX_AGG_REGEX = re.compile(r'rxmpduperampdu (?P<aggregation>[0-9]*)')
TX_AGG_REGEX = re.compile(r' mpduperampdu (?P<aggregation>[0-9]*)')
TX_AGG_STOP_REGEX = re.compile(
r'agg stop reason: tot_agg_tried (?P<agg_tried>[0-9]+) agg_txcancel (?P<agg_canceled>[0-9]+) (?P<agg_stop_reason>.+)'
)
TX_AGG_STOP_REASON_REGEX = re.compile(
r'(?P<reason>\w+) [0-9]+ \((?P<value>[0-9]+%)\)')
MCS_ID = collections.namedtuple(
'mcs_id', ['mode', 'num_streams', 'bandwidth', 'mcs', 'gi'])
MODE_MAP = {'0': '11a/g', '1': '11b', '2': '11n', '3': '11ac'}
BW_MAP = {'0': 20, '1': 40, '2': 80}
def __init__(self, dut, llstats_enabled=True):
self.dut = dut
self.llstats_enabled = llstats_enabled
self.llstats_cumulative = self._empty_llstats()
self.llstats_incremental = self._empty_llstats()
def update_stats(self):
if self.llstats_enabled:
try:
llstats_output = self.dut.adb.shell(self.LLSTATS_CMD,
timeout=1)
self.dut.adb.shell_nb(self.LL_STATS_CLEAR_CMD)
wl_join = self.dut.adb.shell("wl status")
self.bandwidth = int(
re.search(self.BW_REGEX, wl_join).group('bandwidth'))
except:
llstats_output = ''
else:
llstats_output = ''
self._update_stats(llstats_output)
def reset_stats(self):
self.llstats_cumulative = self._empty_llstats()
self.llstats_incremental = self._empty_llstats()
def _empty_llstats(self):
return collections.OrderedDict(mcs_stats=collections.OrderedDict(),
mpdu_stats=collections.OrderedDict(),
summary=collections.OrderedDict())
def _empty_mcs_stat(self):
return collections.OrderedDict(txmpdu=0,
rxmpdu=0,
mpdu_lost=0,
retries=0,
retries_short=0,
retries_long=0)
def _mcs_id_to_string(self, mcs_id):
mcs_string = '{} Nss{} MCS{} GI{}'.format(mcs_id.mode,
mcs_id.num_streams,
mcs_id.mcs, mcs_id.gi)
return mcs_string
def _parse_mcs_stats(self, llstats_output):
llstats_dict = {}
# Look for per-peer stats
match = re.search(self.RX_REGEX, llstats_output)
if not match:
self.reset_stats()
return collections.OrderedDict()
# Find and process all matches for per stream stats
rx_match_iter = re.finditer(self.RX_REGEX, llstats_output)
tx_match_iter = re.finditer(self.TX_REGEX, llstats_output)
tx_per_match_iter = re.finditer(self.TX_PER_REGEX, llstats_output)
for rx_match, tx_match, tx_per_match in zip(rx_match_iter,
tx_match_iter,
tx_per_match_iter):
mode = rx_match.group('mode')
mode = 'HT' if mode == 'MCS' else mode
for nss in [1, 2]:
rx_mcs_iter = re.finditer(self.MCS_REGEX,
rx_match.group(nss + 1))
tx_mcs_iter = re.finditer(self.MCS_REGEX,
tx_match.group(nss + 1))
tx_per_iter = re.finditer(self.MCS_REGEX,
tx_per_match.group(nss + 1))
for mcs, (rx_mcs_stats, tx_mcs_stats,
tx_per_mcs_stats) in enumerate(
itertools.zip_longest(rx_mcs_iter, tx_mcs_iter,
tx_per_iter)):
current_mcs = self.MCS_ID(
mode, nss, self.bandwidth,
mcs + int(8 * (mode == 'HT') * (nss - 1)), 0)
current_stats = collections.OrderedDict(
txmpdu=int(tx_mcs_stats.group('count'))
if tx_mcs_stats else 0,
rxmpdu=int(rx_mcs_stats.group('count'))
if rx_mcs_stats else 0,
mpdu_lost=0,
retries=tx_per_mcs_stats.group('count')
if tx_per_mcs_stats else 0,
retries_short=0,
retries_long=0,
mcs_id=current_mcs)
llstats_dict[self._mcs_id_to_string(
current_mcs)] = current_stats
return llstats_dict
def _parse_mpdu_stats(self, llstats_output):
rx_agg_match = re.search(self.RX_AGG_REGEX, llstats_output)
tx_agg_match = re.search(self.TX_AGG_REGEX, llstats_output)
tx_agg_stop_match = re.search(self.TX_AGG_STOP_REGEX, llstats_output)
rx_fcs_match = re.search(self.RX_FCS_REGEX, llstats_output)
if rx_agg_match and tx_agg_match and tx_agg_stop_match and rx_fcs_match:
agg_stop_dict = collections.OrderedDict(
rx_aggregation=int(rx_agg_match.group('aggregation')),
tx_aggregation=int(tx_agg_match.group('aggregation')),
tx_agg_tried=int(tx_agg_stop_match.group('agg_tried')),
tx_agg_canceled=int(tx_agg_stop_match.group('agg_canceled')),
rx_good_fcs=int(rx_fcs_match.group('rx_good_fcs')),
rx_bad_fcs=int(rx_fcs_match.group('rx_bad_fcs')),
agg_stop_reason=collections.OrderedDict())
agg_reason_match = re.finditer(
self.TX_AGG_STOP_REASON_REGEX,
tx_agg_stop_match.group('agg_stop_reason'))
for reason_match in agg_reason_match:
agg_stop_dict['agg_stop_reason'][reason_match.group(
'reason')] = reason_match.group('value')
else:
agg_stop_dict = collections.OrderedDict(rx_aggregation=0,
tx_aggregation=0,
tx_agg_tried=0,
tx_agg_canceled=0,
rx_good_fcs=0,
rx_bad_fcs=0,
agg_stop_reason=None)
return agg_stop_dict
def _generate_stats_summary(self, llstats_dict):
llstats_summary = collections.OrderedDict(common_tx_mcs=None,
common_tx_mcs_count=0,
common_tx_mcs_freq=0,
common_rx_mcs=None,
common_rx_mcs_count=0,
common_rx_mcs_freq=0,
rx_per=float('nan'))
mcs_ids = []
tx_mpdu = []
rx_mpdu = []
phy_rates = []
for mcs_str, mcs_stats in llstats_dict['mcs_stats'].items():
mcs_id = mcs_stats['mcs_id']
mcs_ids.append(mcs_str)
tx_mpdu.append(mcs_stats['txmpdu'])
rx_mpdu.append(mcs_stats['rxmpdu'])
phy_rates.append(RATE_TABLE[mcs_id.mode][mcs_id.num_streams][
mcs_id.bandwidth][mcs_id.mcs])
if len(tx_mpdu) == 0 or len(rx_mpdu) == 0:
return llstats_summary
llstats_summary['common_tx_mcs'] = mcs_ids[numpy.argmax(tx_mpdu)]
llstats_summary['common_tx_mcs_count'] = numpy.max(tx_mpdu)
llstats_summary['common_rx_mcs'] = mcs_ids[numpy.argmax(rx_mpdu)]
llstats_summary['common_rx_mcs_count'] = numpy.max(rx_mpdu)
if sum(tx_mpdu) and sum(rx_mpdu):
llstats_summary['mean_tx_phy_rate'] = numpy.average(
phy_rates, weights=tx_mpdu)
llstats_summary['mean_rx_phy_rate'] = numpy.average(
phy_rates, weights=rx_mpdu)
llstats_summary['common_tx_mcs_freq'] = (
llstats_summary['common_tx_mcs_count'] / sum(tx_mpdu))
llstats_summary['common_rx_mcs_freq'] = (
llstats_summary['common_rx_mcs_count'] / sum(rx_mpdu))
total_rx_frames = llstats_dict['mpdu_stats'][
'rx_good_fcs'] + llstats_dict['mpdu_stats']['rx_bad_fcs']
if total_rx_frames:
llstats_summary['rx_per'] = (
llstats_dict['mpdu_stats']['rx_bad_fcs'] /
(total_rx_frames)) * 100
return llstats_summary
def _update_stats(self, llstats_output):
self.llstats_cumulative = self._empty_llstats()
self.llstats_incremental = self._empty_llstats()
self.llstats_incremental['raw_output'] = llstats_output
self.llstats_incremental['mcs_stats'] = self._parse_mcs_stats(
llstats_output)
self.llstats_incremental['mpdu_stats'] = self._parse_mpdu_stats(
llstats_output)
self.llstats_incremental['summary'] = self._generate_stats_summary(
self.llstats_incremental)
self.llstats_cumulative['summary'] = self._generate_stats_summary(
self.llstats_cumulative)