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