1#!/usr/bin/env python3.4 2# 3# Copyright 2022 - The Android Open Source Project 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 datetime 18import gzip 19import itertools 20import logging 21import numpy 22import os 23import re 24import shutil 25import subprocess 26import zipfile 27from acts import context 28from pathlib import Path 29 30_DIGITS_REGEX = re.compile(r'-?\d+') 31_TX_PWR_MAX = 100 32_TX_PWR_MIN = -100 33 34 35class LastNrPower: 36 last_time = 0 37 last_pwr = 0 38 39 40class LastLteValue: 41 last_time = 0 42 last_pwr = 0 43 44 45def _string_to_float(input_string): 46 """Convert string to float value.""" 47 try: 48 tmp = float(input_string) 49 except ValueError: 50 print(input_string) 51 tmp = float('nan') 52 return tmp 53 54 55def to_time(time_str): 56 """Convert time string to data time.""" 57 return datetime.datetime.strptime(time_str, '%H:%M:%S.%f') 58 59 60def to_time_sec(time_str, start_time): 61 """"Converts time string to seconds elapsed since start time.""" 62 return (to_time(time_str) - start_time).total_seconds() 63 64 65class LogParser(object): 66 """Base class to parse log csv files.""" 67 68 def __init__(self): 69 self.timestamp_col = -1 70 self.message_col = -1 71 self.start_time = None 72 73 # Dictionary of {log_item_header: log_time_parser} elements 74 self.PARSER_INFO = { 75 r'###[AS] RSRP[': self._parse_lte_rsrp, 76 r'|CC0| PCell RSRP ': self._parse_lte_rsrp2, 77 r'|CC0 Mx0 Sc0| PCell RSRP ': self._parse_lte_rsrp2, 78 r'LT12 PUSCH_Power': self._parse_lte_power, 79 r'UL_PWR(PUSCH)=>pwr_val:': self._parse_lte_power, 80 r'[BM]SSB_RSRP(1)': self._parse_nr_rsrp, 81 r'[BM]SSB_RSRP(0)': self._parse_nr_rsrp, 82 r'###[AS] CurAnt': self._parse_nr_rsrp2, 83 r'[PHY] RF module(': self._parse_fr2_rsrp, 84 #r'[RF] PD : CC0 (monitoring) target_pwr': _parse_fr2_power, 85 #r'[NrPhyTxScheduler][PuschCalcPower] Po_nominal_pusch': _parse_nr_power, 86 r'[NrPhyTxPuschScheduler][PuschCalcPower] Po_nominal_pusch': 87 self._parse_fr2_power, 88 r'[RF NR SUB6] PD : CC0 (monitoring) target_pwr': 89 self._parse_nr_power2, 90 r'[RF NR SUB6] PD : CC1 (monitoring) target_pwr': 91 self._parse_nr_power2, 92 r'[AS] RFAPI_ChangeAntennaSwitch': self._parse_lte_ant_sel, 93 r'[AS] Ant switching': self._parse_lte_ant_sel2, 94 r'###[AS] Select Antenna': self._parse_nr_ant_sel, 95 r'###[AS] CurAnt(': self._parse_nr_ant_sel2, 96 r'[SAR][RESTORE]': self._parse_sar_mode, 97 r'[SAR][NORMAL]': self._parse_sar_mode, 98 r'[SAR][LIMITED-TAPC]': self._parse_tapc_sar_mode, 99 r'###[TAS] [0] ProcessRestoreStage:: [RESTORE]': 100 self._parse_nr_sar_mode, 101 r'###[TAS] [0] ProcessNormalStage:: [NORMAL]': 102 self._parse_nr_sar_mode, 103 r'|CC0| UL Power : PRACH ': self._parse_lte_avg_power, 104 r'activeStackId=0\, [Monitor] txPower ': self._parse_wcdma_power, 105 r'[SAR][DYNAMIC] EN-DC(2) UsedAvgSarLTE': self._parse_sar_values, 106 #r'[SAR][DYNAMIC] UsedAvgSarLTE_100s': self._parse_sar_values2, 107 r'[SAR][DYNAMIC] EN-DC(0) UsedAvgSarLTE': 108 self._parse_lte_sar_values, 109 r'###[TAS] [0] CalcGain:: TotalUsedSar': self._parse_nr_sar_values, 110 r'[SAR][DYNAMIC] IsLTEOn(1) IsNROn(0) ': 111 self._parse_lte_sar_values, 112 r'[SAR][DYNAMIC] IsLTEOn(1) IsNROn(1) ': self._parse_sar_values, 113 r'[MAIN][VolteStatusInd] Volte status ': self._parse_volte_status, 114 r'[PHY] CC0 SLP : dlGrantRatio(3)/ulGrantRatio(3)/RbRatio(3)': 115 self._parse_df_value, 116 r'CC0 AVG: CELLGROUP(0) DCI(D/U):': self._parse_df_value, 117 r'[OL-AIT] band': self._parse_ul_mimo, 118 } 119 120 self.SAR_MODES = [ 121 'none', 'MIN', 'SAV_1', 'SAV_2', 'MAIN', 'PRE_SAV', 'LIMITED-TAPC', 122 'MAX', 'none' 123 ] 124 self.SAR_MODES_DESC = [ 125 '', 'Minimum', 'Slope Saving1', 'Slope Saving2', 'Maintenance', 126 'Pre-Save', 'Limited TAPC', 'Maximum', '' 127 ] 128 129 def parse_header(self, header_line): 130 header = header_line.split(',') 131 try: 132 self.timestamp_col = header.index('PC Time') 133 self.message_col = header.index('Message') 134 except ValueError: 135 print('Error: PC Time and Message fields are not present') 136 try: 137 self.core_id_col = header.index('Core ID') 138 except: 139 self.core_id_col = self.timestamp_col 140 141 def parse_log(self, log_file, gap_options=0): 142 """Extract required data from the exported CSV file.""" 143 144 log_data = LogData() 145 log_data.gap_options = gap_options 146 # Fr-1 as default 147 fr_id = 0 148 149 with open(log_file, 'r') as file: 150 # Read header line 151 header = file.readline() 152 try: 153 self.parse_header(header) 154 except: 155 print('Could not parse header') 156 return log_data 157 158 # Use first message for start time 159 line = file.readline() 160 print(line) 161 line_data = line[1:-2].split('","') 162 if len(line_data) < self.message_col: 163 print('Error: Empty exported file') 164 return log_data 165 166 start_time = to_time(line_data[self.timestamp_col]) 167 168 print('Parsing log file ... ', end='', flush=True) 169 for line in file: 170 line_data = line[1:-2].split('","') 171 if len(line_data) < self.message_col + 1: 172 continue 173 174 message = line_data[self.message_col] 175 if "frIdx 1 " in message: 176 fr_id = 1 177 elif "frIdx 0 " in message: 178 fr_id = 0 179 for line_prefix, line_parser in self.PARSER_INFO.items(): 180 if message.startswith(line_prefix): 181 timestamp = to_time_sec(line_data[self.timestamp_col], 182 start_time) 183 if self.core_id_col == self.timestamp_col: 184 line_parser(timestamp, message[len(line_prefix):], 185 'L1', log_data, fr_id) 186 else: 187 if " CC1 " in message: 188 line_parser(timestamp, 189 message[len(line_prefix):], 'L2', 190 log_data, fr_id) 191 else: 192 line_parser(timestamp, 193 message[len(line_prefix):], 194 line_data[self.core_id_col], 195 log_data, fr_id) 196 break 197 198 if log_data.nr.tx_pwr_time: 199 if log_data.nr.tx_pwr_time[1] > log_data.nr.tx_pwr_time[0] + 50: 200 log_data.nr.tx_pwr_time = log_data.nr.tx_pwr_time[1:] 201 log_data.nr.tx_pwr = log_data.nr.tx_pwr[1:] 202 203 self._find_cur_ant(log_data.lte) 204 self._find_cur_ant(log_data.nr) 205 return log_data 206 207 def get_file_start_time(self, log_file): 208 # Fr-1 as default 209 210 with open(log_file, 'r') as file: 211 # Read header line 212 header = file.readline() 213 try: 214 self.parse_header(header) 215 except: 216 print('Could not parse header') 217 return None 218 219 # Use first message for start time 220 line = file.readline() 221 line_data = line[1:-2].split('","') 222 if len(line_data) < self.message_col: 223 print('Error: Empty exported file') 224 return None 225 226 start_time = to_time(line_data[self.timestamp_col]) 227 return start_time 228 229 def set_start_time(self, line): 230 """Set start time of logs to the time in the line.""" 231 if len(line) == 0: 232 print("Empty Line") 233 return 234 line_data = line[1:-2].split('","') 235 self.start_time = to_time(line_data[self.timestamp_col]) 236 237 def get_message(self, line): 238 """Returns message and timestamp for the line.""" 239 line_data = line[1:-2].split('","') 240 if len(line_data) < self.message_col + 1: 241 return None 242 243 self.line_data = line_data 244 return line_data[self.message_col] 245 246 def get_time(self, line): 247 """Convert time string to time in seconds from the start time.""" 248 line_data = line[1:-2].split('","') 249 if len(line_data) < self.timestamp_col + 1: 250 return 0 251 252 return to_time_sec(line_data[self.timestamp_col], self.start_time) 253 254 def _feed_nr_power(self, timestamp, tx_pwr, option, lte_nr, window, 255 default, interval): 256 if option < 101 and LastNrPower.last_time > 0 and timestamp - LastNrPower.last_time > interval: 257 #print ('window=',window, ' interval=',interval, ' gap=',timestamp-LastNrPower.last_time) 258 ti = LastNrPower.last_time 259 while ti < timestamp: 260 ti += (timestamp - LastNrPower.last_time) / window 261 lte_nr.tx_pwr_time.append(ti) 262 if option == 0: 263 lte_nr.tx_pwr.append(tx_pwr / default) 264 elif option == 1: 265 lte_nr.tx_pwr.append(LastNrPower.last_pwr) 266 elif option == 2: 267 lte_nr.tx_pwr.append((tx_pwr + LastNrPower.last_pwr) / 2) 268 elif option == 3: 269 lte_nr.tx_pwr.append(0) 270 else: 271 lte_nr.tx_pwr.append(option) 272 else: 273 lte_nr.tx_pwr_time.append(timestamp) 274 lte_nr.tx_pwr.append(tx_pwr) 275 LastNrPower.last_time = timestamp 276 LastNrPower.last_pwr = tx_pwr 277 278 def _feed_lte_power(self, timestamp, tx_pwr, log_data, lte_nr, window, 279 default, interval): 280 if log_data.gap_options <= 100 and LastLteValue.last_time > 0 and timestamp - LastLteValue.last_time > interval: 281 #print ('window=',window, ' interval=',interval, ' gap=',timestamp-LastLteValue.last_time) 282 ti = LastLteValue.last_time 283 while ti < timestamp: 284 ti += (timestamp - LastLteValue.last_time) / window 285 lte_nr.tx_pwr_time.append(ti) 286 if log_data.gap_options == 0: 287 lte_nr.tx_pwr.append(tx_pwr / default) 288 elif log_data.gap_options == 1: 289 lte_nr.tx_pwr.append(LastLteValue.last_pwr) 290 elif log_data.gap_options == 2: 291 lte_nr.tx_pwr.append((tx_pwr + LastLteValue.last_pwr) / 2) 292 elif log_data.gap_options == 3: 293 lte_nr.tx_pwr.append(0) 294 else: 295 lte_nr.tx_pwr.append(log_data.gap_options) 296 else: 297 lte_nr.tx_pwr_time.append(timestamp) 298 lte_nr.tx_pwr.append(tx_pwr) 299 LastLteValue.last_time = timestamp 300 LastLteValue.last_pwr = tx_pwr 301 302 def _parse_lte_power(self, timestamp, message, core_id, log_data, fr_id): 303 match = re.search(r'-?\d+', message) 304 if match: 305 tx_pwr = _string_to_float(match.group()) 306 if _TX_PWR_MIN < tx_pwr < _TX_PWR_MAX: 307 self._feed_lte_power(timestamp, tx_pwr, log_data, log_data.lte, 308 20, 1, 1) 309 310 def _parse_lte_rsrp(self, timestamp, message, core_id, log_data, fr_id): 311 data = _DIGITS_REGEX.findall(message) 312 rsrp0 = _string_to_float(data[0]) / 100.0 313 rsrp1 = _string_to_float(data[1]) / 100.0 314 if rsrp0 != 0.0 and rsrp1 != 0.0: 315 log_data.lte.rsrp_time.append(timestamp) 316 log_data.lte.rsrp_rx0.append(rsrp0) 317 log_data.lte.rsrp_rx1.append(rsrp1) 318 319 def _parse_lte_rsrp2(self, timestamp, message, core_id, log_data, fr_id): 320 m = re.search('^\[ ?-?\d+ \((.*?)\)', message) 321 if not m: 322 return 323 data = _DIGITS_REGEX.findall(m.group(1)) 324 if len(data) < 2: 325 return 326 rsrp0 = _string_to_float(data[0]) 327 rsrp1 = _string_to_float(data[1]) 328 if rsrp0 != 0.0 and rsrp1 != 0.0: 329 log_data.lte.rsrp2_time.append(timestamp) 330 log_data.lte.rsrp2_rx0.append(rsrp0) 331 log_data.lte.rsrp2_rx1.append(rsrp1) 332 333 def _parse_nr_rsrp(self, timestamp, message, core_id, log_data, fr_id): 334 index = message.find('rx0/rx1/rx2/rx3') 335 if index != -1: 336 data = _DIGITS_REGEX.findall(message[index:]) 337 log_data.nr.rsrp_time.append(timestamp) 338 log_data.nr.rsrp_rx0.append(_string_to_float(data[4]) / 100) 339 log_data.nr.rsrp_rx1.append(_string_to_float(data[5]) / 100) 340 341 def _parse_nr_rsrp2(self, timestamp, message, core_id, log_data, fr_id): 342 index = message.find('Rsrp') 343 if index != -1: 344 data = _DIGITS_REGEX.findall(message[index:]) 345 log_data.nr.rsrp2_time.append(timestamp) 346 log_data.nr.rsrp2_rx0.append(_string_to_float(data[0]) / 100) 347 log_data.nr.rsrp2_rx1.append(_string_to_float(data[1]) / 100) 348 349 def _parse_fr2_rsrp(self, timestamp, message, core_id, log_data, fr_id): 350 index = message.find('rsrp') 351 data = _DIGITS_REGEX.search(message) 352 module_index = _string_to_float(data.group(0)) 353 data = _DIGITS_REGEX.findall(message[index:]) 354 rsrp = _string_to_float(data[0]) 355 356 if rsrp == 0: 357 return 358 if module_index == 0: 359 log_data.fr2.rsrp0_time.append(timestamp) 360 log_data.fr2.rsrp0.append(rsrp if rsrp < 999 else float('nan')) 361 elif module_index == 1: 362 log_data.fr2.rsrp1_time.append(timestamp) 363 log_data.fr2.rsrp1.append(rsrp if rsrp < 999 else float('nan')) 364 365 def _parse_fr2_power(self, timestamp, message, core_id, log_data, fr_id): 366 data = _DIGITS_REGEX.findall(message) 367 tx_pwr = _string_to_float(data[-1]) / 10 368 if _TX_PWR_MIN < tx_pwr < _TX_PWR_MAX: 369 log_data.fr2.tx_pwr_time.append(timestamp) 370 log_data.fr2.tx_pwr.append(tx_pwr) 371 372 def _parse_nr_power(self, timestamp, message, core_id, log_data, fr_id): 373 data = _DIGITS_REGEX.findall(message) 374 tx_pwr = _string_to_float(data[-1]) / 10 375 if _TX_PWR_MIN < tx_pwr < _TX_PWR_MAX: 376 if core_id == 'L2': 377 self._feed_nr_power(timestamp, tx_pwr, log_data.gap_options, 378 log_data.nr2, 5, 1, 1) 379 else: 380 self._feed_nr_power(timestamp, tx_pwr, log_data.gap_options, 381 log_data.nr, 5, 1, 1) 382 383 def _parse_nr_power2(self, timestamp, message, core_id, log_data, fr_id): 384 if fr_id != 0: 385 return 386 data = _DIGITS_REGEX.findall(message) 387 tx_pwr = _string_to_float(data[0]) / 10 388 if _TX_PWR_MIN < tx_pwr < _TX_PWR_MAX: 389 if core_id == 'L2': 390 self._feed_nr_power(timestamp, tx_pwr, log_data.gap_options, 391 log_data.nr2, 5, 1, 1) 392 else: 393 self._feed_nr_power(timestamp, tx_pwr, log_data.gap_options, 394 log_data.nr, 5, 1, 1) 395 396 def _parse_lte_ant_sel(self, timestamp, message, core_id, log_data, fr_id): 397 data = _DIGITS_REGEX.findall(message) 398 new_ant = _string_to_float(data[1]) 399 old_ant = _string_to_float(data[2]) 400 log_data.lte.ant_sel_time.append(timestamp) 401 log_data.lte.ant_sel_old.append(old_ant) 402 log_data.lte.ant_sel_new.append(new_ant) 403 404 def _parse_lte_ant_sel2(self, timestamp, message, core_id, log_data, 405 fr_id): 406 data = _DIGITS_REGEX.findall(message) 407 if data[0] == '0': 408 log_data.lte.cur_ant_time.append(timestamp) 409 log_data.lte.cur_ant.append(0) 410 elif data[0] == '1': 411 log_data.lte.cur_ant_time.append(timestamp) 412 log_data.lte.cur_ant.append(1) 413 elif data[0] == '10': 414 log_data.lte.cur_ant_time.append(timestamp) 415 log_data.lte.cur_ant.append(1) 416 log_data.lte.cur_ant_time.append(timestamp) 417 log_data.lte.cur_ant.append(0) 418 elif data[0] == '01': 419 log_data.lte.cur_ant_time.append(timestamp) 420 log_data.lte.cur_ant.append(0) 421 log_data.lte.cur_ant_time.append(timestamp) 422 log_data.lte.cur_ant.append(1) 423 424 def _parse_nr_ant_sel(self, timestamp, message, core_id, log_data, fr_id): 425 data = _DIGITS_REGEX.findall(message) 426 log_data.nr.ant_sel_time.append(timestamp) 427 log_data.nr.ant_sel_new.append(_string_to_float(data[1])) 428 log_data.nr.ant_sel_old.append(_string_to_float(data[0])) 429 430 def _parse_nr_ant_sel2(self, timestamp, message, core_id, log_data, fr_id): 431 data = _DIGITS_REGEX.findall(message) 432 log_data.nr.ant_sel_time.append(timestamp) 433 sel_ant = _string_to_float(data[0]) 434 log_data.nr.ant_sel_new.append(sel_ant) 435 if log_data.nr.ant_sel_new: 436 log_data.nr.ant_sel_old.append(log_data.nr.ant_sel_new[-1]) 437 438 def _parse_sar_mode(self, timestamp, message, core_id, log_data, fr_id): 439 sar_mode = len(self.SAR_MODES) - 1 440 for i, mode in enumerate(self.SAR_MODES): 441 if message.startswith('[' + mode + ']'): 442 sar_mode = i 443 log_data.lte.sar_mode_time.append(timestamp) 444 log_data.lte.sar_mode.append(sar_mode) 445 446 def _parse_tapc_sar_mode(self, timestamp, message, core_id, log_data, 447 fr_id): 448 log_data.lte.sar_mode_time.append(timestamp) 449 log_data.lte.sar_mode.append(self.SAR_MODES.index('LIMITED-TAPC')) 450 451 def _parse_nr_sar_mode(self, timestamp, message, core_id, log_data, fr_id): 452 sar_mode = len(self.SAR_MODES) - 1 453 for i, mode in enumerate(self.SAR_MODES): 454 if message.startswith('[' + mode + ']'): 455 sar_mode = i 456 457 log_data.nr.sar_mode_time.append(timestamp) 458 log_data.nr.sar_mode.append(sar_mode) 459 460 def _parse_lte_avg_power(self, timestamp, message, core_id, log_data, 461 fr_id): 462 data = _DIGITS_REGEX.findall(message) 463 tx_pwr = _string_to_float(data[2]) 464 if _TX_PWR_MIN < tx_pwr < _TX_PWR_MAX: 465 log_data.lte.tx_avg_pwr_time.append(timestamp) 466 log_data.lte.tx_avg_pwr.append(tx_pwr) 467 468 def _parse_wcdma_power(self, timestamp, message, core_id, log_data, fr_id): 469 match = re.search(r'-?\d+', message) 470 if match: 471 tx_pwr = _string_to_float(match.group()) / 10 472 if tx_pwr < _TX_PWR_MAX and tx_pwr > _TX_PWR_MIN: 473 log_data.wcdma.tx_pwr_time.append(timestamp) 474 log_data.wcdma.tx_pwr.append(tx_pwr) 475 476 def _parse_sar_values(self, timestamp, message, core_id, log_data, fr_id): 477 data = _DIGITS_REGEX.findall(message) 478 log_data.endc_sar_time.append(timestamp) 479 log_data.endc_sar_lte.append(_string_to_float(data[0]) / 1000.0) 480 log_data.endc_sar_nr.append(_string_to_float(data[1]) / 1000.0) 481 482 def _parse_sar_values2(self, timestamp, message, core_id, log_data, fr_id): 483 data = _DIGITS_REGEX.findall(message) 484 log_data.endc_sar_time.append(timestamp) 485 log_data.endc_sar_lte.append(_string_to_float(data[-3]) / 1000.0) 486 log_data.endc_sar_nr.append(_string_to_float(data[-1]) / 1000.0) 487 488 def _parse_lte_sar_values(self, timestamp, message, core_id, log_data, 489 fr_id): 490 data = _DIGITS_REGEX.findall(message) 491 log_data.lte_sar_time.append(timestamp) 492 log_data.lte_sar.append(_string_to_float(data[0]) / 1000.0) 493 494 def _parse_nr_sar_values(self, timestamp, message, core_id, log_data, 495 fr_id): 496 data = _DIGITS_REGEX.findall(message) 497 log_data.nr_sar_time.append(timestamp) 498 log_data.nr_sar.append(_string_to_float(data[0]) / 1000.0) 499 500 def _parse_df_value(self, timestamp, message, core_id, log_data, fr_id): 501 match = re.search(r' \d+', message) 502 if match: 503 nr_df = _string_to_float(match.group()) 504 log_data.nr.df = (nr_df / 1000 % 1000) / 100 505 log_data.nr.duty_cycle_time.append(timestamp) 506 log_data.nr.duty_cycle.append(log_data.nr.df * 100) 507 else: 508 match = re.search(r'\d+\\,\d+', message) 509 if match: 510 lte_df = match.group(0).split(",") 511 log_data.lte.df = _string_to_float(lte_df[1]) / 100 512 log_data.lte.duty_cycle_time.append(timestamp) 513 log_data.lte.duty_cycle.append(log_data.nr.df * 100) 514 515 def _parse_volte_status(self, timestamp, message, core_id, log_data, 516 fr_id): 517 if message.startswith('[0 -> 1]'): 518 log_data.volte_time.append(timestamp) 519 log_data.volte_status.append(1) 520 elif message.startswith('[3 -> 0]'): 521 log_data.volte_time.append(timestamp) 522 log_data.volte_status.append(0) 523 524 def _parse_ul_mimo(self, timestamp, message, core_id, log_data, fr_id): 525 match = re.search(r'UL-MIMO', message) 526 if match: 527 log_data.ul_mimo = 1 528 529 def _find_cur_ant(self, log_data): 530 """Interpolate antenna selection from antenna switching data.""" 531 if not log_data.cur_ant_time and log_data.ant_sel_time: 532 if log_data.rsrp_time: 533 start_time = log_data.rsrp_time[0] 534 end_time = log_data.rsrp_time[-1] 535 elif log_data.tx_pwr: 536 start_time = log_data.tx_pwr_time[0] 537 end_time = log_data.tx_pwr_time[-1] 538 else: 539 start_time = log_data.ant_sel_time[0] 540 end_time = log_data.ant_sel_time[-1] 541 542 [sel_time, 543 sel_ant] = self.get_ant_selection(log_data.ant_sel_time, 544 log_data.ant_sel_old, 545 log_data.ant_sel_new, 546 start_time, end_time) 547 548 log_data.cur_ant_time = sel_time 549 log_data.cur_ant = sel_ant 550 551 def get_ant_selection(self, config_time, old_antenna_config, 552 new_antenna_config, start_time, end_time): 553 """Generate antenna selection data from antenna switching information.""" 554 sel_time = [] 555 sel_ant = [] 556 if not config_time: 557 return [sel_time, sel_ant] 558 559 # Add data point for the start time 560 if config_time[0] > start_time: 561 sel_time = [start_time] 562 sel_ant = [old_antenna_config[0]] 563 564 # Add one data point before the switch and one data point after the switch. 565 for i in range(len(config_time)): 566 if not (i > 0 567 and old_antenna_config[i - 1] == old_antenna_config[i] 568 and new_antenna_config[i - 1] == new_antenna_config[i]): 569 sel_time.append(config_time[i]) 570 sel_ant.append(old_antenna_config[i]) 571 sel_time.append(config_time[i]) 572 sel_ant.append(new_antenna_config[i]) 573 574 # Add data point for the end time 575 if end_time > config_time[-1]: 576 sel_time.append(end_time) 577 sel_ant.append(new_antenna_config[-1]) 578 579 return [sel_time, sel_ant] 580 581 582class RatLogData: 583 """Log data structure for each RAT (LTE/NR).""" 584 585 def __init__(self, label): 586 587 self.label = label 588 589 self.rsrp_time = [] # RSRP time 590 self.rsrp_rx0 = [] # RSRP for receive antenna 0 591 self.rsrp_rx1 = [] # RSRP for receive antenna 1 592 593 # second set of RSRP logs 594 self.rsrp2_time = [] # RSRP time 595 self.rsrp2_rx0 = [] # RSRP for receive antenna 0 596 self.rsrp2_rx1 = [] # RSRP for receive antenna 1 597 598 self.ant_sel_time = [] # Antenna selection/switch time 599 self.ant_sel_old = [] # Previous antenna selection 600 self.ant_sel_new = [] # New antenna selection 601 602 self.cur_ant_time = [] # Antenna selection/switch time 603 self.cur_ant = [] # Previous antenna selection 604 605 self.tx_pwr_time = [] # TX power time 606 self.tx_pwr = [] # TX power 607 608 self.tx_avg_pwr_time = [] 609 self.tx_avg_pwr = [] 610 611 self.sar_mode = [] 612 self.sar_mode_time = [] 613 614 self.df = 1.0 # Duty factor for UL transmission 615 self.duty_cycle = [] # Duty factors for UL transmission 616 self.duty_cycle_time = [] # Duty factors for UL transmission 617 self.initial_power = 0 618 self.sar_limit_dbm = None 619 self.avg_window_size = 100 620 621 622class LogData: 623 """Log data structure.""" 624 625 def __init__(self): 626 self.lte = RatLogData('LTE') 627 self.lte.avg_window_size = 100 628 629 self.nr = RatLogData('NR CC0') 630 self.nr.avg_window_size = 100 631 632 # NR 2nd CC 633 self.nr2 = RatLogData('NR CC1') 634 self.nr2.avg_window_size = 100 635 636 self.wcdma = RatLogData('WCDMA') 637 638 self.fr2 = RatLogData('FR2') 639 self.fr2.rsrp0_time = [] 640 self.fr2.rsrp0 = [] 641 self.fr2.rsrp1_time = [] 642 self.fr2.rsrp1 = [] 643 self.fr2.avg_window_size = 4 644 645 self.lte_sar_time = [] 646 self.lte_sar = [] 647 648 self.nr_sar_time = [] 649 self.nr_sar = [] 650 651 self.endc_sar_time = [] 652 self.endc_sar_lte = [] 653 self.endc_sar_nr = [] 654 655 self.volte_time = [] 656 self.volte_status = [] 657 658 # Options to handle data gaps 659 self.gap_options = 0 660 661 self.ul_mimo = 0 # Is UL_MIMO 662 663 664class ShannonLogger(object): 665 666 def __init__(self, dut=None, modem_bin=None, filter_file_path=None): 667 self.dm_app = shutil.which(r'DMConsole') 668 self.dut = dut 669 if self.dut: 670 self.modem_bin = self.pull_modem_file() 671 elif modem_bin: 672 self.modem_bin = modem_bin 673 else: 674 raise (RuntimeError, 675 'ShannonLogger requires AndroidDevice or modem binary.') 676 self.filter_file = filter_file_path 677 678 def pull_modem_file(self): 679 local_modem_path = os.path.join( 680 context.get_current_context().get_full_output_path(), 'modem_bin') 681 os.makedirs(local_modem_path, exist_ok=True) 682 try: 683 self.dut.pull_files( 684 '/mnt/vendor/modem_img/images/default/modem.bin', 685 local_modem_path) 686 modem_bin_file = os.path.join(local_modem_path, 'modem.bin') 687 except: 688 self.dut.pull_files( 689 '/mnt/vendor/modem_img/images/default/modem.bin.gz', 690 local_modem_path) 691 modem_zip_file = os.path.join(local_modem_path, 'modem.bin.gz') 692 modem_bin_file = modem_zip_file[:-3] 693 with open(modem_zip_file, 'rb') as in_file: 694 with open(modem_bin_file, 'wb') as out_file: 695 file_content = gzip.decompress(in_file.read()) 696 out_file.write(file_content) 697 return modem_bin_file 698 699 def _unzip_log(self, log_zip_file, in_place=1): 700 log_zip_file = Path(log_zip_file) 701 with zipfile.ZipFile(log_zip_file, 'r') as zip_ref: 702 file_names = zip_ref.namelist() 703 if in_place: 704 zip_dir = log_zip_file.parent 705 else: 706 zip_dir = log_zip_file.with_suffix('') 707 zip_ref.extractall(zip_dir) 708 unzipped_files = [ 709 os.path.join(zip_dir, file_name) for file_name in file_names 710 ] 711 return unzipped_files 712 713 def unzip_modem_logs(self, log_zip_file): 714 log_files = self._unzip_log(log_zip_file, in_place=0) 715 sdm_files = [] 716 for log_file in log_files: 717 if zipfile.is_zipfile(log_file): 718 sdm_files.append(self._unzip_log(log_file, in_place=1)[0]) 719 os.remove(log_file) 720 elif Path( 721 log_file 722 ).suffix == '.sdm' and 'sbuff_power_on_log.sdm' not in log_file: 723 sdm_files.append(log_file) 724 return sorted(set(sdm_files)) 725 726 def _export_single_log(self, file): 727 temp_file = str(Path(file).with_suffix('.csv')) 728 if self.filter_file: 729 export_cmd = [ 730 self.dm_app, 'traceexport', '-c', '-csv', '-f', 731 self.filter_file, '-b', self.modem_bin, '-o', temp_file, file 732 ] 733 else: 734 export_cmd = [ 735 self.dm_app, 'traceexport', '-c', '-csv', '-b', self.modem_bin, 736 '-o', temp_file, file 737 ] 738 logging.debug('Executing: {}'.format(export_cmd)) 739 subprocess.call(export_cmd) 740 return temp_file 741 742 def _export_logs(self, log_files): 743 csv_files = [] 744 for file in log_files: 745 csv_files.append(self._export_single_log(file)) 746 return csv_files 747 748 def _filter_log(self, input_filename, output_filename, write_header): 749 """Export log messages from input file to output file.""" 750 log_parser = LogParser() 751 with open(input_filename, 'r') as input_file: 752 with open(output_filename, 'a') as output_file: 753 754 header_line = input_file.readline() 755 log_parser.parse_header(header_line) 756 if log_parser.message_col == -1: 757 return 758 759 if write_header: 760 output_file.write(header_line) 761 # Write next line for time reference. 762 output_file.write(input_file.readline()) 763 764 for line in input_file: 765 message = log_parser.get_message(line) 766 if message: 767 for filter_str in log_parser.PARSER_INFO: 768 if message.startswith(filter_str): 769 output_file.write(line) 770 break 771 772 def _export_filtered_logs(self, csv_files): 773 start_times = [] 774 log_parser = LogParser() 775 reordered_csv_files = [] 776 for file in csv_files: 777 start_time = log_parser.get_file_start_time(file) 778 if start_time: 779 start_times.append(start_time) 780 reordered_csv_files.append(file) 781 print(reordered_csv_files) 782 print(start_times) 783 file_order = numpy.argsort(start_times) 784 print(file_order) 785 reordered_csv_files = [reordered_csv_files[i] for i in file_order] 786 print(reordered_csv_files) 787 log_directory = Path(reordered_csv_files[0]).parent 788 exported_file = os.path.join(log_directory, 'modem_log.csv') 789 write_header = True 790 for file in reordered_csv_files: 791 self._filter_log(file, exported_file, write_header) 792 write_header = False 793 return exported_file 794 795 def _parse_log(self, log_file, gap_options=0): 796 """Extract required data from the exported CSV file.""" 797 log_parser = LogParser() 798 log_data = log_parser.parse_log(log_file, gap_options=0) 799 return log_data 800 801 def process_log(self, log_zip_file): 802 sdm_log_files = self.unzip_modem_logs(log_zip_file) 803 csv_log_files = self._export_logs(sdm_log_files) 804 exported_log = self._export_filtered_logs(csv_log_files) 805 log_data = self._parse_log(exported_log, 0) 806 for file in itertools.chain(sdm_log_files, csv_log_files): 807 os.remove(file) 808 return log_data 809