1#!/usr/bin/env python3
  2
  3"""Command line packet viewer."""
  4
  5import operator
  6import functools
  7import itertools
  8import ast
  9import textwrap
 10from datetime import datetime
 11from datetime import timedelta
 12
 13from chart.common.traits import Datatype
 14
 15from chart.project import SID
 16from chart.project import settings
 17from chart.db.model.table import find_param_by_name
 18from chart.products.scos2000.srdb_viewer import find_packetdefs
 19from chart.db.model.table import TableInfo
 20from chart.db.model.field import FieldInfo
 21from chart.db.model.field import Display
 22from chart.db.ddl.ddl_tool import standard_items
 23from chart.products.pus.packetdef import PacketDef
 24
 25from chart.common.prettyprint import Table
 26from chart.common.xml import datetime_to_xml
 27
 28# Reference time for timepstamps used in parameters
 29OBT_REFTIME = datetime(1970, 1, 1)
 30
 31# limits used to revert to large number Exponential format
 32EXPO_UPPER = 1.0e11
 33EXPO_LOWER = -1.0e11
 34
 35def format_number(value):
 36    """Format large values to expontial format."""
 37
 38    if isinstance(value, (float, int)):
 39        if value > EXPO_UPPER or value < EXPO_LOWER:
 40            # force required exponential format
 41            return ('%.16e' %value)
 42
 43    return value
 44
 45
 46# create a formated string that looks like a table with columns with fixed max width
 47# given by wrap_width
 48def str_table(
 49    rows,
 50    has_header=False,
 51    header_char="-",
 52    delim=" | ",
 53    justify="left",
 54    separate_rows=False,
 55    prefix="",
 56    postfix="",
 57    wrap_width=50):
 58    """Indents a table by column.
 59       - rows: A sequence of sequences of items, one sequence per row.
 60       - hasHeader: True if the first row consists of the columns' names.
 61       - headerChar: Character to be used for the row separator line
 62         (if hasHeader==True or separateRows==True).
 63       - delim: The column delimiter.
 64       - justify: Determines how are data justified in their column.
 65         Valid values are 'left','right' and 'center'.
 66       - separateRows: True if rows are to be separated by a line
 67         of 'headerChar's.
 68       - prefix: A string prepended to each printed row.
 69       - postfix: A string appended to each printed row.
 70       - wrap_width: Width for wrapping text; each element in
 71         the table is first wrapped by function wrap, below."""
 72
 73    # closure for breaking logical rows to physical, using wrapfunc
 74    def row_wrapper(row):
 75        new_rows = [wrap(item, wrap_width).split("\n") for item in row]
 76        return [[substr or "" for substr in item] for item in itertools.zip_longest(*new_rows)]
 77
 78    # break each logical row into one or more physical ones
 79    logical_rows = [row_wrapper(row) for row in rows]
 80    # columns of physical rows
 81    columns = itertools.zip_longest(*functools.reduce(operator.add, logical_rows))
 82    # get the maximum of each column by the string length of its items
 83    max_widths = [max([len(str(item)) for item in column]) for column in columns]
 84    row_separator = header_char * (
 85        len(prefix) + len(postfix) + sum(max_widths) + len(delim) * (len(max_widths) - 1)
 86    )
 87    # select the appropriate justify method
 88    justify = {"center": str.center, "right": str.rjust, "left": str.ljust}[
 89        justify.lower()
 90    ]
 91
 92    output = ''
 93    if separate_rows:
 94        output += row_separator +'\n'
 95    for physicalRows in logical_rows:
 96        for row in physicalRows:
 97            output += prefix + delim.join(
 98                [justify(str(item), width) for (item, width) in zip(row, max_widths)]
 99            ) + postfix +'\n'
100        if separate_rows or has_header:
101            output += row_separator +'\n'
102            has_header = False
103    return output
104
105
106# written by Mike Brown      http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
107def wrap(text, width):
108    """
109    A word-wrap function that preserves existing line breaks
110    and most spaces in the text. Expects that existing line
111    breaks are posix newlines (\n).
112    """
113    lines = []
114    if text is None:
115        return '\n'.join(lines)
116
117    for paragraph in text.split('\n'):
118        line = []
119        len_line = 0
120        for word in paragraph.split(' '):
121            len_word = len(word)
122
123            if len_word >= width:
124                # split word across required lines
125                for line in textwrap.wrap(word, width):
126                    lines.append(line)
127                line = []
128
129            else:
130                if len_line + len_word <= width:
131                    line.append(word)
132                    len_line += len_word + 1
133                else:
134                    lines.append(' '.join(line))
135                    line = [word]
136                    len_line = len_word + 1
137
138        if len(line) > 0:
139            lines.append(' '.join(line))
140
141    return '\n'.join(lines)
142
143
144def find_field_by_key(table_info, key):
145    """Return FieldInfo based on key."""
146
147    found = False
148    for f in table_info.fields.values():
149        if f.key == key:
150            found = True
151            field_info = f
152            break
153    if found:
154        return field_info
155    else:
156        return None
157
158
159def get_standard_fields(table):
160    """Return a list fo Table Standard Field names."""
161
162    # get standard fields
163    standard = standard_items(table_type=table.table_type,
164                              storage=table.storage,
165                              pattern=table.pattern,
166                              sparse=table.sparse,
167                              stats=False)
168
169    standard_fields = []
170
171    # extract from field_info the field name
172    for f in standard.fields:
173        if f is SID:
174            standard_fields.append('SID')
175
176        else:
177            standard_fields.append(f.name)
178
179    return standard_fields
180
181
182def get_table_details(table_name):
183    """Return Table Info and Standard fields."""
184
185    standard_fields = []
186    table_info = TableInfo(table_name)
187
188    if table_info.storage_table is not None:
189        for name, field_info in table_info.storage_table.all_fields.items():
190            standard_fields.append(field_info)
191
192    return table_info, standard_fields
193
194
195def get_standard_field_info(table_name, field):
196    """Return requested Standard field_info."""
197
198    table_info = TableInfo(table_name)
199
200    if table_info.storage_table is not None:
201        for name, field_info in table_info.storage_table.all_fields.items():
202            if name.upper() == field.upper():
203                return field_info
204
205    return None
206
207
208
209def apply_choices(fieldinfo, raw_v):
210    """Return choice for value or list of values."""
211
212    # check for list
213    if isinstance(raw_v, list):
214        cal_v_ = []
215        for val in raw_v:
216            if isinstance(val, str):
217                x=float(val)
218            else:
219                x=val
220
221            try:
222                # check for further embedded list
223                if isinstance(x, list):
224                    for y in x:
225                        cal_v_.append(fieldinfo.choices.get_name(y))
226                else:
227                    cal_v_.append(fieldinfo.choices.get_name(x))
228
229            except KeyError:
230                cal_v_.append('KeyError')
231
232        cal_v = cal_v_
233
234    elif '[' in str(raw_v):
235        # list, calibrate each item
236        vals = list(raw_v.replace('[', '').replace(']','').split(","))
237        cal_v = []
238        for val in vals:
239            if isinstance(val, str):
240                cal_v.append(fieldinfo.choices.get_name(float(val)))
241            else:
242                cal_v.append(fieldinfo.choices.get_name(val))
243
244    else:
245        # calibrate single item
246        if raw_v is None:
247            cal_v = ''
248        else:
249            if isinstance(raw_v, str):
250                cal_v = raw_v
251            else:
252                cal_v = fieldinfo.choices.get_name(raw_v)
253
254    return cal_v
255
256
257def    make_packet_row(raw_row,
258                    sid,
259                    fieldinfos,
260                    packet_rows,
261                    inc_time_col=False,
262                    sensing_time=None,
263                    create_time=None,
264                    table='TM',
265                    packet_time=None):
266    """Create packet contents row, applying choices where appropriate."""
267
268    table_info = TableInfo(table)
269    gen_time_col = str(sensing_time)
270    recp_time_col = str(create_time)
271    packet_time_col = str(packet_time)
272    pidrefs = {}
273
274    for raw_v, fieldinfo in zip(raw_row, fieldinfos):
275        # prepare data row for each parameter
276        name = fieldinfo.name
277        desc = '' if fieldinfo.description is None else fieldinfo.description
278        unit = '' if fieldinfo.unit is None else fieldinfo.unit
279
280        # calibrated value
281        if fieldinfo.calibration_name is None:
282            cal_name = ''
283            cal_v = raw_v
284
285            # cal value: if param of type DATETIME and raw_v is float convert to datetime
286            if fieldinfo.datatype is Datatype.DATETIME and isinstance(raw_v, float):
287                # cal value, convert to datetime
288                cal_name = 'datetime'
289                cal_v = datetime_to_xml(
290                    OBT_REFTIME  + timedelta(seconds=raw_v), include_us=True)
291
292        else:
293            if isinstance(raw_v, list):
294                cal_v = []
295                for val in raw_v:
296                    cal_v.append(fieldinfo.cal_value(sid, float(val)))
297
298            elif '[' in str(raw_v):
299                # list, calibrate each item
300                vals = list(raw_v.replace('[', '').replace(']','').split(","))
301                cal_v = []
302                for val in vals:
303                    cal_v.append(fieldinfo.cal_value(sid, float(val)))
304
305            else:
306                cal_v = fieldinfo.cal_value(sid, float(raw_v))
307
308            cal_name = fieldinfo.calibration_name
309
310        # convert cal value to choice, where applicable
311        if fieldinfo.choices is not None:
312            cal_v = apply_choices(fieldinfo, cal_v)
313            cal_name = fieldinfo.choices.description
314
315
316        if fieldinfo.display is Display.PIDREF:
317            # convert deduced parameters, where applicable
318            # get field info of deduced parameter
319            if isinstance(raw_v, str) and '[' in str(raw_v):
320                # convert to list
321                raw_v = list(raw_v.replace('[', '').replace(']','').split(","))
322
323            if isinstance(raw_v, list):
324                cal_v = []
325                for val in raw_v:
326                    if find_field_by_key(table_info, val) is not None:
327                        cal_v.append(find_field_by_key(table_info, val).name)
328                    else:
329                        cal_v.append(val)
330
331            else:
332                if find_field_by_key(table_info, raw_v) is not None:
333                    cal_v = find_field_by_key(table_info, raw_v).name
334                else:
335                    cal_v = raw_v
336
337            # store reference param names for calibration below
338            pidrefs[fieldinfo.name] = cal_v
339
340
341        if fieldinfo.datatype is Datatype.DEDUCED:
342            # calibrate using deduced parameter details, where applicable
343            pidref = pidrefs[fieldinfo.related]
344            if isinstance(raw_v, str) and '[' in str(raw_v):
345                # convert to list
346                raw_v = list(raw_v.replace('[', '').replace(']','').split(","))
347
348            if isinstance(raw_v, list):
349                cal_v = []
350                cal_name = []
351                for val, param in zip(raw_v, pidref):
352                    ded_fieldinfo = find_param_by_name(str(param))
353
354                    # calibrate and apply choices, where appropriate
355                    if ded_fieldinfo.choices is not None:
356                        cal_v.append(ded_fieldinfo.choices.get_name(ded_fieldinfo.cal_value(sid, float(val))))
357                        cal_name.append(ded_fieldinfo.choices.description)
358
359                    else:
360                        if ded_fieldinfo.calibration_name is None:
361                            # no calibration so append raw value
362                            cal_name.append('')
363                            cal_v.append(val)
364                        else:
365                            cal_v.append(ded_fieldinfo.cal_value(sid, float(val)))
366                            cal_name.append(ded_fieldinfo.calibration_name)
367
368            else:
369                ded_fieldinfo = find_param_by_name(pidref)
370                if ded_fieldinfo.calibration_name is None:
371                    # no calibration so append raw value
372                    cal_name.append('')
373                    cal_v.append(val)
374                else:
375                    cal_v.append(ded_fieldinfo.cal_value(sid, float(val)))
376                    cal_name.append(ded_fieldinfo.calibration_name)
377
378        # output data row
379        if inc_time_col:
380            # only once per packet, and only add reception_time if different to generation_time/(sensing_time)
381            if sensing_time == create_time:
382                packet_rows.append((gen_time_col, name, desc, str(format_number(raw_v)), str(format_number(cal_v)), unit,  str(cal_name)))
383            else:
384                if settings.TM_PACKET_TIME:
385                    packet_rows.append((
386                        gen_time_col, recp_time_col, packet_time_col, name, desc,
387                        str(format_number(raw_v)), str(format_number(cal_v)), unit,  str(cal_name)))
388
389                else:
390                    packet_rows.append((gen_time_col, recp_time_col, name, desc, str(format_number(raw_v)), str(format_number(cal_v)), unit,  str(cal_name)))
391
392            if gen_time_col == str(sensing_time):
393                gen_time_col = ''
394            if recp_time_col == str(create_time):
395                recp_time_col = ''
396            if packet_time_col == str(packet_time):
397                packet_time_col = ''
398
399        else:
400            packet_rows.append((name, desc, str(format_number(raw_v)), str(format_number(cal_v)), unit,  cal_name))
401
402    return packet_rows
403
404
405def add_empty_arrays(table, packet, raw_row):
406    """For display purposes and empy arrays in packets, to show full packet structure."""
407
408    fieldinfos = []
409    raw_names = []
410    raw_values = []
411
412    # flags to indicate if to increment dimensions (nested level) of missing parameters.
413    # If the nested level above has no value then do not increment level of nesting
414    dim1_has_no_value = False
415    dim2_has_no_value = False
416
417    # get dict of params from raw_row
418    params = ast.literal_eval(str(raw_row))
419
420    # for each param create lists of fieldInfos and values
421    for param, value in zip(params.keys(), params.values()):
422        fieldinfos.append(find_param_by_name(param, table=table, multiple=False))
423        raw_names.append(param)
424        raw_values.append(value)
425
426    if settings.PUS_SHOW_EMPTY_ARRAYS:
427        #for packet in packet_def:
428            for paramdef in packet.params:
429                if paramdef.field_info is not None and paramdef.field_info.name not in params.keys():
430                    fieldinfos.append(paramdef.field_info)
431                    raw_names.append(paramdef.field_info.name)
432
433                    # add empty array, dimension depending on nesting level
434                    if paramdef.field_info.max_dimension == 1:
435                        raw_values.append([])
436                        dim1_has_no_value = True
437
438                    elif paramdef.field_info.max_dimension == 2:
439                        if dim1_has_no_value:
440                            # add the parameter but do not increment dimension
441                            raw_values.append([])
442                            dim1_has_no_value = False
443
444                        else:
445                            # add parameter with incremented dimension
446                            raw_values.append([[]])
447
448                        dim2_has_no_value = True
449
450                    elif paramdef.field_info.max_dimension == 3:
451                        if dim2_has_no_value:
452                            # add the parameter but do not increment dimension
453                            raw_values.append([[]])
454                            dim2_has_no_value = False
455
456                    else:
457                        # add parameter with incremented dimension
458                        raw_values.append([[[]]])
459
460
461    # complete list of fields and values
462    return fieldinfos, raw_values, raw_names
463
464
465
466def    display_packet(packet_def,
467                   param_data,
468                   scid,
469                   sensing_time,
470                   create_time,
471                   packet_time,
472                   table='TM',
473                   inc_time_col=False,
474                   inc_header=True):
475    """Display packet contents."""
476
477    packet_table = []
478
479    # add empty arrays, as required
480    fieldinfos, raw_row, _ = add_empty_arrays(table, packet_def, param_data)
481
482    # get packet details
483    make_packet_row(raw_row,
484                    SID(scid),
485                    fieldinfos,
486                    packet_table,
487                    inc_time_col,
488                    sensing_time,
489                    create_time,
490                    table,
491                    packet_time)
492
493    # prepare output text table
494    if inc_time_col:
495        if inc_header:
496            if sensing_time == create_time:
497                headings=('Sensing_Time', 'Name', 'Description', 'RawValue',
498                        'CalValue','Unit','Cal')
499
500            else:
501                if table == 'TC':
502                    headings=('Release_Time', 'Execution_Time', 'Name', 'Description',
503                            'RawValue', 'CalValue','Unit','Cal')
504                elif settings.TM_PACKET_TIME:
505                    headings=('Sensing_Time', 'Reception_Time', 'Generation_Time', 'Name',
506                            'Description', 'RawValue', 'CalValue','Unit','Cal')
507
508                else:
509                    headings=('Sensing_Time', 'Reception_Time', 'Name',
510                            'Description', 'RawValue', 'CalValue','Unit','Cal')
511
512
513        else:
514            if sensing_time == create_time:
515                headings=('           ', '    ', '           ', '        ',
516                        '        ','    ','   ')
517            elif settings.TM_PACKET_TIME:
518                headings=('           ', '           ', '           ', '    ',
519                        '           ', '        ', '        ','    ','   ')
520            else:
521                headings=('           ', '           ', '    ', '           ', '        ', '        ','    ','   ')
522
523    else:
524        if inc_header:
525            headings=('Name', 'Description', 'RawValue', 'CalValue','Unit','Cal')
526        else:
527            headings=('    ', '           ', '        ', '        ','    ','   ')
528
529    packet_table_str = str_table([headings] + packet_table,
530                                 has_header=inc_header,
531                                 wrap_width=50)
532
533    return packet_table_str
534
535
536def    build_packet_table(table,
537                       raw_rows,
538                       #packetdefs,
539                       sid):
540
541    # prepare table
542    spid_head = None
543    res = ''
544
545    # for each row to process
546    for raw_row in raw_rows:
547        # get packet details, based on spid
548        if 'TM' in table or 'EV' in table:    # ['TM', 'EV']:
549            packet = PacketDef.find_one(spid=raw_row[1])
550            #packetdef = find_packetdefs(packetdefs,
551            #                        spids=[raw_row[1]])
552
553        elif 'TC' in table:    # in ['TC']:
554            packet = PacketDef.find_one(name=raw_row[1])
555            #packetdef = find_packetdefs(packetdefs,
556            #                        name=raw_row[1])
557
558        if raw_row[1] != spid_head:
559            # New, or first SPID, generate header
560            #for packet in packetdef.values():
561            res += '\n'+  \
562                       'SPID: ' + packet.name +  \
563                       ', APID: ' + str(packet.apid) + \
564                       ', Service: ' + str(packet.service) + \
565                       ', SubService: ' + str(packet.subservice) + \
566                       ', Description: ' + packet.description +'\n\n'
567
568            # prepare for new packet type
569            spid_head = raw_row[1]
570            inc_header = True
571
572        # generate packet output
573        res += display_packet(packet,
574                              str(raw_row[2]),
575                              sid.name,
576                              raw_row[0],
577                              raw_row[3],
578                              packet_time = raw_row[4] if settings.TM_PACKET_TIME else None,
579                              table=table,
580                              inc_time_col=True,
581                              inc_header=inc_header)
582        # for next packet
583        inc_header = False
584
585    return res
586
587
588def    build_raw_packet_table(table,
589                           fields,
590                           raw_rows,
591                           packetdefs,
592                           sid):
593
594    # prepare table
595    spid_head = None
596    res = ''
597
598    # for each row to process
599    for raw_row in raw_rows:
600        # get packet details, based on spid
601        if 'TM' in table or 'EV' in table:    # ['TM_STORE', 'EV_STORE']:
602            # get packet details, based on spid
603            packetdef = find_packetdefs(packetdefs,
604                                        spids=[raw_row[1]])
605
606            # add empty arrays, as required
607            fieldinfos, raw_values, params = add_empty_arrays(table, packetdef, raw_row[2])
608
609            if raw_row[1] != spid_head:
610                # New, or first SPID, generate header
611                for packet in packetdef.values():
612                    res = '\n'+  \
613                       'SPID: ' + packet.name +  \
614                       ', APID: ' + str(packet.apid) + \
615                       ', Service: ' + str(packet.service) + \
616                       ', SubService: ' + str(packet.subservice) + \
617                       ', Description: ' + packet.description +'\n\n'
618
619                # prepare for new packet type
620                spid_head = raw_row[1]
621
622            # prepare payload for output
623            res += str(raw_row[0]) + '\n'
624            res += '{'
625            for param, value in zip(params, raw_values):
626                res += "'" + param + "': " + str(value) +", \n "
627            res += "}\n"
628            res = res.replace(", \n }", "}") + '\n'
629
630
631        elif 'TC' in table:        # in ['TC_STORE', 'TC_ACK_STORE']:
632            # get packet details, based on name
633            packetdef = find_packetdefs(packetdefs,
634                                        name=raw_row[1])
635
636            # add empty arrays, as required
637            fieldinfos, raw_values, params = add_empty_arrays(table, packetdef, raw_row[2])
638
639            # prepare payload
640            res = '{'
641            for param, value in zip(params, raw_values):
642                res += "'" + param + "': " + str(value) +", "
643            res += "}"
644            res = res.replace(", }", "}")
645
646            t = Table(headings=('Parameter', 'Value'))
647            for n, v in zip(fields, raw_row):
648                if n.upper() ==  'PAYLOAD':
649                    t.append((n, res))
650                elif type(v) is memoryview:
651                    t.append((n, v.tobytes()))
652                else:
653                    t.append((n, str(v)))
654
655            t.write()
656            res = ''
657
658    return res