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