1#!/usr/bin/env python3
2
3"""Representation of XML files from the db/ts/packets directory used by SCOS-based
4projects to configure which CCSDS packets write to which tables."""
5
6import urllib.parse
7import logging
8from collections import namedtuple
9from collections import OrderedDict
10from enum import Enum
11import itertools
12import time
13
14from chart.project import SID
15from chart.common.path import Path
16from chart.common.xml import XMLElement
17from chart.db.model.table import TableInfo
18from chart.db.model.field import FieldInfo
19from chart import settings
20from chart.common.decorators import memoized2
21from chart.common.exceptions import ConfigError
22from chart.project import project
23from chart.common.traits import Datatype
24from chart.db.model.field import Display
25from chart.products.pus.packetdef_criteria import PacketDefCriteriaLookup
26
27# Show the user a message reporting how long it took to initialise
28# table, if over this threshold
29TIME_REPORT_THRESHOLD = 1.0
30
31ELEM_PACKET = 'packet'
32ELEM_APID = 'apid'
33ELEM_SERVICE = 'service'
34ELEM_SUBSERVICE = 'subservice'
35ELEM_PARAM1 = 'param1'
36ELEM_PARAM2 = 'param2'
37ELEM_HEADER_TYPE = 'header-type'
38ELEM_PACKET_PARAMS = 'packet-params'
39ELEM_PACKET_DEF_ELEMENT = 'packetdef'
40ELEM_PI1_OFFSET = 'param1-offset'
41ELEM_PI1_WIDTH = 'param1-width'
42ELEM_PI2_OFFSET = 'param2-offset'
43ELEM_PI2_WIDTH = 'param2-width'
44ELEM_BYTE = 'byte' # PLF (3)
45ELEM_BIT = 'bit' # PLF (4)
46ELEM_DECODER = 'decoder'
47ELEM_NAME = 'name' # PLF (1)
48ELEM_FIELD = 'field'
49ELEM_TABLE = 'table'
50ELEM_DESCRIPTION = 'description'
51ELEM_SPID = 'spid' # PLF (2)
52ELEM_GROUP_SIZE = 'group-size'
53ELEM_OFFSET_MOD = 'offset-mod'
54ELEM_PADDING = 'padding'
55ELEM_DYNAMIC_START = 'dynamic-start'
56ELEM_EV_FORMAT = 'format'
57ELEM_LENGTH = 'length'
58ELEM_DIMENSION = 'dimension'
59
60# Packet Post Processing
61ELEM_NBOCC = 'num-occ' # PLF (5)
62ELEM_LGOCC = 'bits-between-occ' # PLF (6)
63ELEM_TIME_OFFSET = 'time-offset' # PLF (7)
64ELEM_TDOCC = 'time-delta-occ' # PLF (8)
65
66# XML schema used for packet definition files
67PACKET_SCHEMA = 'packet.xsd'
68
69
70# All packets with these services go into generic tables for respective services
71# GENERIC = [1, 5]
72
73# XML file extension
74EXT_XML = '.xml'
75
76# Decoder = Enum('Decoder','UP_TO_PEC')
77
78logger = logging.getLogger()
79
80
81class NoSuchPacketDef(BaseException):
82 pass
83
84
85# information about a particular parameter to be read from a source packet -
86# the target table field and the location to read from
87# PacketField = namedtuple('PacketField', 'field datatype length byte bit')
88# PacketField = namedtuple('PacketField', 'field byte bit decoder')
89
90# Locations and widths of PI1 and PI2 parameters
91# ParamLocation = namedtuple('ParamLocation', 'param1_offset param1_width param2_offset
92# param2_width')
93
94# For service 5 packets that need to be identified by Param2, we look up the offset and width
95# of Param2 in a packet, using the dictionary below. If the key (PID_STYPE, PID_APID, PID_PI1_VAL)
96# is not in the dictionary, we set Param2 to None.
97# (PID_STYPE, PID_APID, PID_PI1_VAL): (PIC_PI2_OFF, PIC_PI2_WID)
98
99# Classify packet definitions according to the nature of their static or dynamic parameters
100# (experimental... not used yet)
101class PacketQuant(Enum):
102 FIXED = 'fixed' # no dynamics
103 BLOCKS = 'blocks' # repeating fixed blocks of parameters
104 MEMORY = 'memory' # a single variable length list of bytes
105 PARAMETERISED = 'parameterised' # list of specified parameters
106
107
108class PacketHeaderType(Enum):
109 """Record the basic design of the packet that a PacketDef represents."""
110
111 # For telecommand packets the string values map directly to the "Packet header name" column of
112 # the CCF file
113
114 # Standard telecommand
115 CCSDS_TC = 'TC'
116 # Standard telemetry
117 CCSDS_TM = 'TM'
118
119 # Standard CCSDS TM headers - Project Specific, depending on settings in project specif SRDD MIBs
120 CCSDS_STDXCCSD = settings.CCSDS_STDXCCSD
121 # PUS Header - no CCSDS packet?
122 CCSDS_STDXPUSS = settings.CCSDS_STDXPUSS
123
124 # Packet with no header - TC Packet type
125 CCSDS_NO_HDR = settings.CCSDS_NO_HDR
126
127 # Nominal telecommand CCSDS packet
128 CCSDS_NOM_TC = 'NOM_TC'
129
130 PSS_TC = 'PSS_TC'
131 HIPRI_TC = 'HIPRI_TC'
132 HIPSS_TC = 'HIPSS_TC'
133
134 # TC_HDR0 -> CCSDS_STDXCCSD
135 # IICA0001 -> CCSDS_STDXCCSD
136 # Auxiliary telecommand CCSDS packet
137 CCSDS_AU_TC = 'AU_TC'
138 # Control CCSDS packet
139 CCSDS_CONTROL = 'CONTROL'
140 # ???
141 CCSDS_HPC1_TC = 'HPC1_TC'
142
143# For each header type record the type of CCSDS-derived class we should instantiate
144# PacketHeaderType.CCSDS_NOM_TC.packet = CCSDSTM
145
146
147class ParamDef:
148 """PUS parameter definition."""
149 def __init__(self,
150 table_info=None,
151 node=None,
152 field_info=None,
153 byte=None,
154 bit=None,
155 pos=None,
156 tpsd=None,
157 fixed_repeats=None,
158 # pidref=None,
159 # offset_to_next=None,
160 offset_mod=0,
161 field_name=None,
162 group_size=None,
163 # width=None,
164 num_occ=1,
165 bits_between_occ=0,
166 td_occ=0,
167 time_offset=0,
168 length=None,
169 dimension=0,
170 ):
171 """Args:
172 `table_info` (TableInfo): Not stored, but needed when reading from XML
173 to look up fields within tables
174 `field_info` (FieldInfo): Field information
175 `byte` (int)': Byte position for fixed parameters. From PLF_OFFBY
176 `bit` (int): Bit position for fixed parameters. From PLF_OFFBI
177 `pos` (int): Position of this parameter within the variable part of the packet.
178 From VPD_POS
179 `tpsd` (int): From VPD_TPSD. Should match the packet SPID unless it's a packet
180 using the parameter group selection mechanism
181 `fixed_repeats` (int): This is a virtual parameter specifying number of repeats for
182 successive blocks. From VPD_FIXREP
183 # `pidref` (bool): This parameter contains the PID identifying a packet
184 `offset_to_next` (int): Delta in bits from the start of this parameter to the start of
185 of the next. If None, compute from datatype. From VPD_OFFSET
186 'field_name` (str): From VPD_PIDREF
187 `group_size` (int): Mark this field as a sizer to repeat the next n fields
188 From VPD_GRPSIZE
189 `num_occ` (int): From PLF_NBOCC, Number of occurances of parameter, if >1 super-commutative
190 `bits_between_occ` (int): From PLF_LGOCC, Number of bits between successive occurances of parameter
191 `td_occ` (int): From PLF_TDOCC, Time delay (in milliseconds) between 2 consecutive occurrences
192 (if num_occ > 1)
193 `time_offset` (int): From PLF_TIME, Time offset of first parameter occurrence relative to packet
194 time (in milliseconds). This equally applies to parameters
195 which are super-commutated or not.
196 `length` (int): For spacer parameters only give the length in bits
197 `dimension` (int): dimension of the parameter in this packet
198
199 # VPD_CHOICE
200 # VPD_FORM
201 """
202 self.field_info = field_info
203 self.byte = byte
204 self.bit = bit
205 self.pos = pos
206 self.tpsd = tpsd
207 self.fixed_repeats = fixed_repeats
208 self.offset_mod = offset_mod
209 # self.offset_to_next = offset_to_next
210 self.field_name = field_name
211 # the SRDB uses "0" to declare fields are not group sizers
212 self.group_size = group_size if group_size != 0 else None
213
214 # Number of occurrences, for parameters with a fixed number of repeats defined
215 self.num_occ = num_occ
216 # Stride between occurrences
217 self.bits_between_occ = bits_between_occ
218 # Timedelta between occurances
219 self.td_occ = td_occ
220 # Time delta between nominal packet time and the first occurrence
221 self.time_offset = time_offset
222 self.length = length
223 self.dimension = dimension
224
225 if node is not None:
226 # fieldname = node.parse_str(ELEM_NAME)
227 # self.field_info = table_info.fields[fieldname]
228 # self.byte = node.parse_int(ELEM_BYTE, None)
229 # self.bit = node.parse_int(ELEM_BIT, None)
230 # self.group_size = node.parse_int(ELEM_GROUP_SIZE, None)
231 # self.offset_to_next = node.parse_int(ELEM_OFFSET_TO_NEXT, None)
232 for child in node.elem.iterchildren():
233 if child.tag == ELEM_NAME:
234 self.field_info = table_info.fields[child.text]
235
236 elif child.tag == ELEM_BYTE:
237 self.byte = int(child.text)
238
239 elif child.tag == ELEM_BIT:
240 self.bit = int(child.text)
241
242 elif child.tag == ELEM_GROUP_SIZE:
243 self.group_size = int(child.text)
244
245 elif child.tag == ELEM_OFFSET_MOD:
246 self.offset_mod = int(child.text)
247
248 elif child.tag == ELEM_NBOCC:
249 self.num_occ = int(child.text)
250
251 elif child.tag == ELEM_LGOCC:
252 self.bits_between_occ = int(child.text)
253
254 elif child.tag == ELEM_TIME_OFFSET:
255 self.time_offset = int(child.text)
256
257 elif child.tag == ELEM_TDOCC:
258 self.td_occ = int(child.text)
259
260 elif child.tag == ELEM_LENGTH:
261 self.length = int(child.text)
262
263 elif child.tag == ELEM_DIMENSION:
264 self.dimension = int(child.text)
265
266 if self.bit is None:
267 self.bit = 0
268
269 # if hasattr(self.field_info, 'width') and self.field_info.width is not None:
270 # found in JCS that final offset mod should be field_info.width - offset_mod !
271 # TBD Must be checked, e.g. see SPID 32305
272 # self.offset_mod = self.field_info.width - self.offset_mod
273
274 if self.offset_to_next is None:
275 self.offset_to_next = self.field_info.length
276
277 def __str__(self):
278 if self.field_info is None:
279 return 'ParamDef({n} {B}/{b} - {g}-{d})'.format(
280 n='SpaceParam',
281 B=self.byte,
282 b=self.bit,
283 g=self.group_size,
284 d=0)
285
286 return 'ParamDef({n} {B}/{b} - {g}-{d})'.format(
287 n=self.field_info.name,
288 B=self.byte,
289 b=self.bit,
290 g=self.group_size,
291 d=self.field_info.max_dimension)
292
293 def write_xml(self, node):
294 """Write ourselves as a child of param XML table `node`."""
295 param_node = node.add(tag=ELEM_FIELD)
296 if self.field_info is not None:
297 param_node.add(tag=ELEM_NAME, text=self.field_info.name)
298
299 if self.byte is not None:
300 param_node.add(tag=ELEM_BYTE, text=self.byte)
301
302 if self.bit is not None and self.bit != 0:
303 param_node.add(tag=ELEM_BIT, text=self.bit)
304
305 if hasattr(self.field_info, 'width') and self.field_info.width is not None:
306 # 14 VPD_OFFSET Number(6) 'self.field_info.width' Number of bits between the start
307 # position of this parameter and the end bit of the previous parameter in the packet.
308 # A positive offset enables the introduction of a 'gap' between the previous parameter
309 # and this one. A negative offset enables the 'overlap' of the bits contributing to
310 # this parameter with the ones contributing to the previous parameter(s).
311 # Therefore use the following algorithm to reset offset_mod according the above
312 self.offset_mod = self.field_info.width + self.offset_mod - \
313 (self.field_info.length if self.field_info.length is not None else 0)
314
315 if self.offset_mod != 0:
316 param_node.add(tag=ELEM_OFFSET_MOD, text=self.offset_mod)
317
318 if self.group_size is not None:
319 param_node.add(tag=ELEM_GROUP_SIZE, text=self.group_size)
320
321 if self.num_occ != 1:
322 param_node.add(tag=ELEM_NBOCC, text=self.num_occ)
323
324 if self.bits_between_occ != 0:
325 param_node.add(tag=ELEM_LGOCC, text=self.bits_between_occ)
326
327 if self.td_occ != 0 and self.num_occ > 1:
328 param_node.add(tag=ELEM_TDOCC, text=self.td_occ)
329
330 if self.time_offset != 0:
331 param_node.add(tag=ELEM_TIME_OFFSET, text=self.time_offset)
332
333 if self.length is not None:
334 param_node.add(tag=ELEM_LENGTH, text=self.length)
335
336 if self.dimension != 0:
337 param_node.add(tag=ELEM_DIMENSION, text=self.dimension)
338
339 def offset_to_next(self):
340 """Return the delta in bits between out start position and the start of the next field."""
341 # pus = self.field_info.pus_data_length()
342 if self.field_info.datatype is Datatype.DEDUCED:
343 # should not happen at runtime. The packet format displayer in packets tool
344 # may call if though
345 return None
346
347 else:
348 return self.field_info.pus_data_length() + self.offset_mod
349
350
351class ParamDefList:
352 """Represent a list of static and dynamic parameters, ingested into one table."""
353
354 def __init__(self, table_info=None, node=None, packet_name=None, sid=None):
355 self.table_info = table_info
356 self.params = []
357 self.dynamic_params = []
358 self.end_padding = None
359
360 if node is not None:
361 # read from XML
362 # logger.debug('Begin read tables')
363 import time
364
365 # import gc
366 # gc.disable()
367 # start = time.perf_counter()
368 if sid is None:
369 self.table_info = project.sid().table(node.parse_str(ELEM_NAME), fast_load=True)
370 else:
371 self.table_info = sid.table(node.parse_str(ELEM_NAME), fast_load=True)
372
373 # dur = time.perf_counter() - start
374 # gc.enable()
375 # if dur > 1:
376 # logger.debug('Read table definitions in {s:.5}s'.format(s=dur))
377
378 for field_node in node.findall(ELEM_FIELD):
379 param_def = ParamDef(node=field_node, table_info=self.table_info)
380 # For Telemetry
381 # we assume the initial list of parameters (with <byte> set) will all be static,
382 # followed by all the dynamic parameters
383 # For Telecommands, all parameters have a byte field and dynamic_params list not applicable
384 if param_def.byte is not None:
385 self.params.append(param_def)
386
387 else:
388 self.dynamic_params.append(param_def)
389
390 self.end_padding = node.parse_int(ELEM_PADDING, None)
391
392 def write_xml(self, parent_node):
393 """Write ourselves under `parent_node`."""
394 table_node = parent_node.add(tag=ELEM_TABLE)
395 table_node.add(tag=ELEM_NAME, text=self.table_info.name)
396 # offset = None
397 for param_def in itertools.chain(self.params, self.dynamic_params):
398 # if offset is not None:
399 # param_def.offset = offset
400
401 # super_comm, time_offset =
402 param_def.write_xml(table_node)
403 # if hasattr(param_def.field_info, 'width'):
404 # offset = param_def.field_info.width - param_def.field_info.length
405 # logger.debug('field {f} has width {w} len {l} so setting offset of next field to\
406 # {o}'.format(
407 # f=param_def.field_info.name, w=param_def.field_info.width,
408 # l=param_def.field_info.length,
409 # o=offset))
410
411 # else:
412 # if offset is not None:
413 # logger.debug('Reset offset')
414 # offset = None
415
416 # if offset is not None:
417 # table_node.add(tag=ELEM_PADDING, text=offset)
418
419 # return super_comm, time_offset
420
421
422class PacketDef:
423 """PUS packet definition."""
424
425 # Main cache to prevent PacketReaders being instantiated many times
426 # _criteria_cache = None
427
428 # cache of SPID against PacketDef to speed up lookup by spid
429 # _spid_cache = {}
430
431 # location of fixed Field objects (params) within this packet
432 # StaticParam = namedtuple('PositionedParam', 'byte bit paramdef')
433
434 # location of variable / dynamic params within this packet
435 # VariableParam = namedtuple(
436 # 'VariableParam',
437 # 'pos name grpsize fixrep choice pidref disdesc width justify newline dchar form offset')
438
439 def __init__(self,
440 path=None,
441 name=None,
442 description=None,
443 service=None,
444 subservice=None,
445 apid=None,
446 spid=None,
447 param1=None,
448 param2=None,
449 tpsd=None,
450 dynamic_start=None,
451 header_type=PacketHeaderType.CCSDS_TM,
452 ev_format=None,
453 super_comm=False,
454 time_split=False,
455 sid=None,
456 pkt_type=None, # !!!!! todo, fix the name and comment and datatype !!!!!
457 ):
458 """Args:
459
460 `path` (Path): XML definition to instantiate from
461 `name` (str): Name of packet. From TPCF_NAME
462 `description` (str): Description of packet. From PID_DESCR
463 `service` (int): Service number. From PID_TYPE
464 `subservice` (int): Subservice number. From PID_STYPE
465 `apid` (int): APID Application Packet IDentifier. From PID_APID
466 `spid` (int): SPID (acronym?) Unique packet ID code. From PID_SPID
467 `param1` (int): First packet identifier. From PID_PI1_VAL
468 `param2` (int): Second packet identifier. From PID_PI2_VAL
469 `tpsd` (int): For variable packets, identity of the VPD definition.
470 Normally equal to SPID but not required, especially for packets with multiple
471 payload definitions. From PID_TPSD
472 `dynamic_start` (int): For variable packets, for byte for variable processing.
473 From PID_DFHSIZE
474 `ev_format` (Display): Record encoding used in Event packets (XDR or compressed XDR)
475 `sid` (object): sources sid object, to uniquely identify the packets
476 `pkt_type` (str): contains packet type information.
477 """
478 # Create object from scratch
479 self.name = name
480 self.description = description
481 self.service = service
482 self.subservice = subservice
483 self.apid = apid
484 self.spid = spid
485 self.param1 = param1
486 self.param2 = param2
487 self.tpsd = tpsd
488 self.dynamic_start = dynamic_start
489 self.header_type = header_type
490 self.ev_format = ev_format
491 self.super_comm = super_comm
492 self.time_split = time_split
493 self.sid = sid
494 self.pkt_type = pkt_type
495
496 # ParamDefList
497 self.paramlists = []
498
499 if path is not None:
500 # Initialise from XML file
501 # self.name = path.stem
502 try:
503 root_node = XMLElement(filename=path)
504 except OSError:
505 raise NoSuchPacketDef()
506
507 self.name = root_node.parse_str(ELEM_NAME, None)
508 self.description = root_node.parse_str(ELEM_DESCRIPTION, None)
509 self.spid = root_node.parse_int(ELEM_SPID, None)
510 self.service = root_node.parse_int(ELEM_SERVICE, None)
511 self.subservice = root_node.parse_int(ELEM_SUBSERVICE, None)
512 self.apid = root_node.parse_int(ELEM_APID, None)
513 self.param1 = root_node.parse_int(ELEM_PARAM1, None)
514 self.param2 = root_node.parse_int(ELEM_PARAM2, None)
515 self.dynamic_start = root_node.parse_int(ELEM_DYNAMIC_START, None)
516 header_type_text = root_node.parse_str(ELEM_HEADER_TYPE, None)
517 if header_type_text is not None:
518 self.header_type = PacketHeaderType(header_type_text)
519
520 if root_node.parse_str(ELEM_EV_FORMAT, None) is not None:
521 self.ev_format = Display(root_node.parse_str(ELEM_EV_FORMAT, None))
522
523 for table_node in root_node.findall(ELEM_TABLE):
524 self.paramlists.append(
525 ParamDefList(node=table_node, packet_name=self.name, sid=sid)
526 )
527
528 if len(self.paramlists) > 0:
529 # check for super-commutated parameters in packet
530 for param in self.paramlists[0].params:
531 if param.num_occ > 1:
532 self.super_comm = True
533 break
534
535 # check for time-split parameters in packet
536 for param in self.paramlists[0].params:
537 if param.time_offset != 0:
538 self.time_split = True
539 break
540
541 # if self.tpsd is not None and self.spid != self.tpsd:
542 # logger.warn('SPID ({spid}) does not match TPSD ({tpsd})'.format(
543 # spid=self.spid, tpsd=self.tpsd))
544
545 def __str__(self):
546 return 'PacketReader({n} {s}/{ss}/{ap}/{sp}/{p1}/{p2})'.format(
547 n=self.name,
548 s=self.service,
549 ss=self.subservice,
550 ap=self.apid,
551 sp=self.spid,
552 p1=self.param1,
553 p2=self.param2)
554
555 @property
556 def params(self):
557 """Return all parameters."""
558 for paramlist in self.paramlists:
559 for param in itertools.chain(paramlist.params, paramlist.dynamic_params):
560 yield param
561
562 def paramlist(self, table_info):
563 for paramlist in self.paramlists:
564 if paramlist.table_info == table_info:
565 return paramlist
566
567 result = ParamDefList(table_info=table_info)
568 self.paramlists.append(result)
569 return result
570
571 def write_xml(self, output, report=False):
572 """Write ourselves to `output` file with optional `report` to terminal."""
573 root_node = XMLElement(tag=ELEM_PACKET)
574 root_node.set_schema(PACKET_SCHEMA)
575 root_node.add(tag=ELEM_NAME, text=self.name)
576 root_node.add(tag=ELEM_DESCRIPTION, text=self.description)
577 if self.spid is not None:
578 root_node.add(tag=ELEM_SPID, text=self.spid)
579
580 if self.service is not None:
581 root_node.add(tag=ELEM_SERVICE, text=self.service)
582
583 if self.subservice is not None:
584 root_node.add(tag=ELEM_SUBSERVICE, text=self.subservice)
585
586 if self.apid is not None:
587 root_node.add(tag=ELEM_APID, text=self.apid)
588
589 if self.dynamic_start is not None:
590 root_node.add(tag=ELEM_DYNAMIC_START, text=self.dynamic_start)
591
592 # not all packets have param 1 read configured
593 # if param_sources is not None and param_sources.param1_offset is not None:
594 if self.param1 is not None:
595 root_node.add(tag=ELEM_PARAM1, text=self.param1)
596
597 # not all packets have param 2 read configured
598 # if param_sources is not None and param_sources.param2_offset is not None:
599 if self.param2 is not None:
600 root_node.add(tag=ELEM_PARAM2, text=self.param2)
601
602 root_node.add(tag=ELEM_HEADER_TYPE, text=self.header_type.value)
603
604 if self.ev_format is not None:
605 root_node.add(tag=ELEM_EV_FORMAT, text=self.ev_format)
606
607 # <table><name>a</><field>...</></>
608 for param_def_list in self.paramlists:
609 param_def_list.write_xml(root_node)
610
611 root_node.write(output, pretty_print=True, report=report)
612 @memoized2
613 def tables(self):
614 """Return an ordered dictionary of tableinfo against list of PacketFields."""
615 # sometimes it might be useful to have a lightweight version
616 # that only returns the TableInfos
617 result = {} # use ordinary dictionay as faster OrderedDict()
618 for paramlist in self.paramlists:
619 if paramlist.table_info:
620 try:
621 field_info = paramlist.table_info.fields
622
623 except KeyError:
624 raise KeyError('Table {t} does not contain field although packet '
625 'definition {p} specifies it'.format(
626 t=paramlist.table_info.name,
627 p=self.name))
628 result[paramlist.table_info] = field_info
629 return result
630
631
632 @property
633 def browse_url(self):
634 """Return a URL to a page describing this field."""
635 from django.urls import reverse
636 url = urllib.parse.urlparse(settings.CHART_WEB)
637 return '{scheme}://{loc}{local}?SID={sid}&PKT_TYPE={pkt_type}&SPID={spid}'.format(
638 scheme=url.scheme,
639 loc=url.netloc,
640 local=reverse('db:packet',kwargs={'packet_name': self.name}),
641 sid=self.sid,
642 pkt_type=self.pkt_type.value,
643 spid=self.spid,
644 )
645
646 # @staticmethod
647 # def find_one(service=None, subservice=None, apid=None, param1=None, param2=None, spid=None):
648 # """Yield all PacketReaders matching requested pattern."""
649
650 # if spid is not None:
651 # result = PacketDef._spid_cache.get(spid)
652 # if result is not None:
653 # return result
654
655 # try:
656 # result = PacketDef(settings.TM_PACKET_DIR.joinpath(str(spid) + EXT_XML))
657 # except NoSuchPacketDef:
658 # return None
659
660 # PacketDef._spid_cache[spid] = result
661 # logger.debug('Loaded packetdef {spid} into cache'.format(spid=spid))
662 # return result
663
664 # if PacketDef._criteria_cache is None:
665 # PacketDef._criteria_cache = {}
666 # # PacketDef._spid_cache = {}
667 # for pd in PacketDef.all():
668 # PacketDef._criteria_cache[(pd.service, pd.subservice, pd.apid, pd.param1, pd.param2)] = pd
669 # PacketDef._spid_cache[pd.spid] = pd
670
671 # return PacketDef._criteria_cache.get((service, subservice, apid, param1, param2))
672
673
674# When retrieving PacketDef objects specify the domain of packets - TM and TC are fully
675# independant, but can share service/subservice/APID
676# TC don't have SPIDs
677class PacketDomain(Enum):
678 TM = 'tm'
679 TC = 'tc'
680 EV = 'ev'
681
682PacketDomain.TM.description = 'Telemetry'
683PacketDomain.TC.description = 'Telecommand'
684PacketDomain.EV.description = 'Event'
685
686def all(domain, sid=None):
687 if domain is PacketDomain.TM:
688 for filename in sid.db_dir(settings.TM_PACKET_SUBDIR).glob('*' + EXT_XML):
689 yield PacketDef(path=filename,sid=sid, pkt_type=domain)
690
691 elif domain is PacketDomain.TC:
692 for filename in sid.db_dir(settings.TC_PACKET_SUBDIR).glob('*' + EXT_XML):
693 yield PacketDef(path=filename, sid=sid, pkt_type=domain)
694
695 elif domain is PacketDomain.EV:
696 # not sid specific (at the moment)
697 for filename in settings.EV_PACKET_SUBDIR.glob('*' + EXT_XML):
698 yield PacketDef(path=filename, sid=sid, pkt_type=domain)
699
700 else:
701 raise ValueError()
702
703PacketDef.all = all
704
705
706def find_one(
707 sid=None,
708 domain=PacketDomain.TM, # should remove this default
709 spid=None,
710 name=None,
711 service=None,
712 subservice=None,
713 apid=None,
714 param1=None,
715 param2=None,
716):
717 """Retrieve a list of matching PacketDef(s) from the specified domain.
718
719 If `single` is set then return exactly one result, or None if not found."""
720 # The only queries we really allow are:
721 # - TM SPID lookup
722 # - TM criteria lookup
723 # - TC name lookup
724 # Other permutations are not currently needed and not really supported
725 if domain is PacketDomain.TM:
726 # TM SPID find
727 if spid is not None:
728 result = find_one.tm_spid_cache.get((sid, spid))
729 if result is not None:
730 return result
731
732 else:
733 try:
734 result = PacketDef(
735 sid.db_dir(settings.TM_PACKET_SUBDIR).joinpath(str(spid) + EXT_XML),
736 sid=sid,
737 )
738 except NoSuchPacketDef:
739 return None
740
741 find_one.tm_spid_cache[(sid, spid)] = result
742 return result
743
744 # TM criteria find
745 if find_one.tm_criteria_cache is None:
746 find_one.tm_criteria_cache = {}
747 for pd in PacketDef.all(domain,sid):
748 find_one.tm_criteria_cache[
749 (sid.name,pd.service, pd.subservice, pd.apid, pd.param1, pd.param2)
750 ] = pd
751
752 result = find_one.tm_criteria_cache.get(
753 (sid.name,service, subservice, apid, param1, param2)
754 )
755 return result
756
757 elif domain is PacketDomain.TC:
758 if name is not None:
759 result = find_one.tc_name_cache.get((sid, name))
760 if result is not None:
761 return result
762
763 else:
764 try:
765 result = PacketDef(
766 sid.db_dir(settings.TC_PACKET_SUBDIR).joinpath(name + EXT_XML),
767 sid=sid
768 )
769
770 except NoSuchPacketDef:
771 return None
772
773 find_one.tc_name_cache[(sid, name)] = result
774 return result
775
776 elif domain is PacketDomain.EV:
777 if spid is not None:
778 result = find_one.ev_spid_cache.get((sid, spid))
779 if result is not None:
780 return result
781
782 else:
783 try:
784 result = PacketDef(
785 settings.EV_PACKET_SUBDIR.joinpath(str(spid) + EXT_XML),
786 sid=sid
787 )
788 except NoSuchPacketDef:
789 return None
790
791 find_one.ev_spid_cache[(sid, spid)] = result
792 return result
793
794 else:
795 raise ValueError()
796
797
798find_one.tm_spid_cache = {}
799find_one.tm_criteria_cache = None
800find_one.tc_name_cache = {}
801find_one.ev_spid_cache = {}
802find_one.criteria_cache = None
803
804PacketDef.find_one = find_one