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