1#!/usr/bin/env python2
  2
  3"""Implementation of CCSDS class."""
  4
  5from abc import ABC
  6from abc import abstractmethod
  7import logging
  8import sys
  9from datetime import datetime
 10from datetime import timedelta
 11from binascii import hexlify
 12
 13from chart.common.exceptions import ConfigError
 14from chart.products.pus.packet_criteria import PacketCriteria
 15from chart.products.pus.packetdef import PacketDef
 16from chart.products.pus.packetdef import PacketHeaderType
 17from chart.common.binary import pack_uint16_be
 18from chart.common.binary import unpack_uint16_be
 19from chart.common.binary import unpack_uint22_be
 20from chart.common.binary import unpack_uint24_be
 21from chart.common.binary import pack_uint32_be
 22from chart.common.binary import unpack_uint32_be
 23from chart.project import settings
 24from chart.project import SID
 25from chart.products.utils import Segment
 26from chart.products.pus.exceptions import CCSDSBadHeader
 27
 28logger = logging.getLogger()
 29
 30# for int.from_bytes calls
 31BYTEORDER = 'big'
 32
 33# Map from OBT time to UTC
 34# This is the GPS reference time less number of leap seconds so far
 35CUC_REFTIME = datetime(1980, 1, 6) - timedelta(seconds=17)
 36
 37# APID for Idle Packets, which can be discarded
 38IDLE_PACKET_APID = 2047
 39
 40# First Header Pointer value indicating no CCSDS packet begins in this frame
 41FHP_UNUSED = 2047
 42
 43# The true length of a CCSDS packet is 7 bytes more than the dflength field
 44DFLENGTH_OFFSET = 7
 45
 46# Only allow reset_sensing_time() to be called for packets which are long enough
 47CCSDSSTDXPUSS_LEN = 10
 48
 49# We need this due to special format of HPTM packet, S3 specific
 50# Note: there is no APID= 29 for MTG or JCS  defined, so do not require specific check
 51SERVICE_HPTM = 0
 52SUBSERVICE_HPTM = 0
 53APID_HPTM = 29
 54
 55class CCSDS(Segment):
 56    """Base class for specific CCSDS types."""
 57
 58    @staticmethod
 59    def make_packet(header_type: PacketHeaderType, buff: bytes):
 60        """Construct a CCSDS packet object, based on `header_type`, using binary `buff`."""
 61        if header_type in [PacketHeaderType.CCSDS_NOM_TC, PacketHeaderType.CCSDS_STDXPUSS]:
 62            return CCSDSSTDXPUSS(buff)
 63            # return CCSDSTM(buff)
 64
 65        elif header_type is PacketHeaderType.CCSDS_STDXCCSD:
 66            return CCSDSXCCSD(buff)
 67
 68        elif header_type is PacketHeaderType.CCSDS_NO_HDR:
 69            return CCSDS_NO_HDR(buff)
 70
 71        else:
 72            raise NotImplementedError()
 73
 74    def has_pec(self):
 75        return False
 76
 77    def datetime_params_use_reference_time(self):
 78        """Specify source of referent time values for relative datetime parameters in this packet.
 79
 80        For parameters with a coarse time defined, we use a standard fixed reference time.
 81
 82        But for parameters with no coarse time defined, some packets may be configured to use the
 83        OBT of the packet itself as a referent time for datatime parameters."""
 84        return False
 85
 86
 87    def reset_sensing_time(self, sensing_time):
 88        """Only applicable to TM packets, see below."""
 89        return sensing_time
 90
 91
 92class CCSDSXCCSD(CCSDS):
 93    """CCSDS packet with 6-byte XCCSD header. `buffer` can be much bigger than the packet.
 94
 95    We don't access anything beyond packet end. Call `length()` to find how big the packet actually
 96    is.
 97
 98    If `buff` is None then service and subservice must be set and a synthetic packet is    created.
 99    `header` is used to pull out the HPTM timestamps if needed.
100    """
101    HEADER_SKIP = 6  # 10
102
103    def payload(self):
104        return self.buff[self.HEADER_SKIP:]
105
106    @property
107    def version_number(self):
108        """Get Version Number (=0)."""
109        return self.buff[0] >> 5
110
111    @property
112    def type_flag(self):
113        """Get Type Flag."""
114        return (self.buff[0] >> 4) & 1
115        # return (ord(self.buff[0]) >> 4) & 1
116
117    @property
118    def dfh_flag(self):
119        """Get DFH Flag."""
120        return (self.buff[0] >> 3) & 1
121        # return (ord(self.buff[0]) >> 3) & 1
122
123    def get_apid(self):
124        """Get Application ID."""
125        return unpack_uint16_be(self.buff, 0) & 0x7ff
126        # return UINT16_BE.unpack(self.buff[:2])[0] & 0x7ff
127
128    def set_apid(self, apid):
129        """Set Application ID."""
130        assert apid >= 0 and apid <= 16383
131        exist = unpack_uint16_be(self.buff, 0)
132        exist = exist & 0xf800
133        exist = exist | apid
134        pack_uint16_be(self.buff, 0, exist)
135
136    apid = property(get_apid, set_apid)
137
138    @property
139    def pid(self):
140        """Process ID, stored in top 7 bits of APID."""
141        return self.apid >> 4
142
143    @property
144    def cat(self):
145        """CATegory, stored in bottom 4 bits of APID."""
146        return self.apid & 0xf
147
148    @property
149    def grouping_flags(self):
150        """Get Sequence Grouping Flags."""
151        return self.buff[2] >> 6
152        # return ord(self.buff[2]) >> 6
153
154    @property
155    def sequence_count(self):
156        """Get Sequence Count."""
157        return unpack_uint16_be(self.buff, 2) & 0x3fff
158
159    @property
160    def dflength(self):
161        """Retrieve the Data Field Length."""
162        try:
163            return unpack_uint16_be(self.buff, 4)
164        except IndexError:
165            raise CCSDSBadHeader('dflength', len(self.buff), 8)
166
167        # return int.from_bytes(self.buff, byteorder=BYTEORDER)
168
169    def get_length(self):
170        """Get packet length in bytes computed from the Packet DF Length field + 7."""
171        return self.dflength + DFLENGTH_OFFSET
172        # return unpack_uint16_be(self.buff, 4) + 7
173
174    def set_length(self, length):
175        """Set Packet Data Field Length to `length` - 7. This does not adjust the actual
176        buffer length."""
177        assert length >= 7
178        pack_uint16_be(self.buff, 4, length - DFLENGTH_OFFSET)
179
180    length = property(get_length, set_length)
181
182    @property
183    def obt_single(self):
184        return None
185
186    def write(self, handle):
187        """Store the (possibly modified) CCSDS packet to `handle` as a raw packet,no header."""
188
189        length = self.length
190        handle.write(self.buff[:length])
191        return length
192
193    def show_header(self, target=sys.stdout, indent=''):
194        """Display basic packet header information to stdout."""
195        target.write("""{i}Version: {version}
196{i}Type: {type}
197{i}DFH flag: {dfh}
198{i}APID: {apid} ({pid}/{cat})
199{i}Group flags: {group}
200{i}Sequence count: {count}
201{i}Length: {dflength} ({length})
202""".format(
203    i=indent,
204    version=self.version_number,
205    type=self.type_flag,
206    dfh=self.dfh_flag,
207    apid=self.apid,
208    pid=self.pid,
209    cat=self.cat,
210    group=self.grouping_flags,
211    count=self.sequence_count,
212    length=self.length,
213    dflength=self.dflength,
214))
215
216
217class CCSDSSTDXPUSS(CCSDSXCCSD):
218    """6-byte CCSDS header + 4-byte PUS header."""
219
220    HEADER_SKIP = 10
221    def payload(self):
222        return self.buff[self.HEADER_SKIP:]
223    # CCSDS + 4-byte PUS header
224
225    def datetime_params_use_reference_time(self):
226        """Specify source of referent time values for relative datetime parameters in this packet.
227
228        For parameters with a coarse time defined, we use a standard fixed reference time.
229
230        But for parameters with no coarse time defined, some packets may be configured to use the
231        OBT of the packet itself as a referent time for datatime parameters."""
232
233        return False
234
235    def get_service(self):
236        """Get Service Type."""
237        # S3 APID = 29 is HPTM packet with special format
238        # return self.buff[7]
239        try:
240            return self.buff[7] if self.apid != APID_HPTM else SERVICE_HPTM
241        except IndexError:
242            raise CCSDSBadHeader('service', len(self.buff), 7)
243
244    def set_service(self, service):
245        """Set Service value."""
246        assert 0 <= service and service < 256
247        self.buff[7] = service
248        # self.buff[7] = chr(service)
249
250    service = property(get_service, set_service)
251
252    def get_subservice(self):
253        """Get Subservice Type."""
254        # S3 APID = 29 is HPTM packet with special format
255        if self.apid == APID_HPTM:
256            return SUBSERVICE_HPTM
257
258        try:
259            return self.buff[8]
260        except IndexError as e:
261            raise CCSDSBadHeader('no subservice', len(self.buff), 9)
262
263    def set_subservice(self, subservice):
264        """Set Subservice value."""
265        assert 0 <= subservice and subservice < 256
266        self.buff[8] = subservice
267        # self.buff[8] = chr(subservice)
268
269    subservice = property(get_subservice, set_subservice)
270
271    def checksum(self):
272        """Compute correct checksum for this packet. Not Applicable for TC Packet"""
273        return None
274
275    @property
276    def destination_id(self):
277        """Get Destination ID."""
278        if len(self.buff) > 9:
279            # some CADU packets do  not have destination_id set
280            return int(str(self.buff[9]))
281
282        return None
283        #return ord(self.buff[9])
284
285    def show_header(self, target=sys.stdout, indent=''):
286        """Display basic packet header information to stdout."""
287        super(CCSDSSTDXPUSS, self).show_header(target, indent)
288        target.write("""{i}Service: {stype}
289{i}Subservice: {sstype}
290""".format(i=indent,
291           stype=self.service,
292           sstype=self.subservice))
293
294class CCSDSTM(CCSDSSTDXPUSS):
295    # Standard 20+ byte CCSDS packet with CCSDS header, PUS header, OBT, time status and PEC
296    #  0: Primary Header (6)
297    #  6: PUS header (4)
298    # 10: OBT (7)
299    # 17: Time status (1)
300    # 18+: PEC (2)
301
302    # See comments in products/pus/ccsds.py param1() function. These two items need to be
303    # removed and made non-static. This is not the way to pass values around
304    packet_identifier = None
305    sid = None
306
307    def datetime_params_use_reference_time(self):
308        """Specify source of referent time values for relative datetime parameters in this packet.
309
310        For parameters with a coarse time defined, we use a standard fixed reference time.
311
312        But for parameters with no coarse time defined, some packets may be configured to use the
313        OBT of the packet itself as a referent time for datatime parameters."""
314        return self.dfh_flag == 1
315
316    @property
317    def obt(self):
318        """Retrieve OBT as a tuple of (seconds, 1/2^16 seconds, 1/2^24 seconds)."""
319        # APID = 29 is HPTM packet with special format - return OBT = 0
320        # Check with Mike, this modified code is correct for CORTEX data,
321        #  may also be applicable to EDDS data
322        # Update: surely use DFH flag instead?
323        if self.apid == APID_HPTM:
324            s = unpack_uint32_be(self.buff, 16-2)      # See S3-ID-TAF-SC-00438, Table 7-5 HPTM packet Format
325            return s, 0, 0
326            ##  return (0, 0, 0)
327
328        if len(self.buff) < 17:
329            # time status packet
330            return None
331
332        s = unpack_uint32_be(self.buff, 10)  # s
333        t = unpack_uint16_be(self.buff, 14)  # 1/2^16s
334        u = self.buff[16]  # 1/2^24s
335        return s, t, u
336
337    @property
338    def obt_single(self):
339        """Retrieve OBT as a single 56 bit counter."""
340        s, t, u = self.obt
341        return (s * 1 << 24) + (t * (1 << 8)) + u
342
343    @property
344    def obt_fractional(self):
345        """Retrieve OBT time as a floating point value with seconds and parts of seconds."""
346        if self.obt is None:
347            return None
348
349        s, t, u = self.obt
350        return s + (t/(2**16)) + (u/(2**24))
351
352    def has_pec(self):
353        if self.dfh_flag == 1:
354            return self.dfh_flag == 1 if self.apid != APID_HPTM else False
355        else:
356            return False
357
358    def checksum(self):
359        """Compute correct checksum for this packet."""
360        return self.crc_bitwise()
361
362    def crc_bitwise(self):
363        """Compute the cyclic redundancy checksum.
364
365        Adapted from ECSS-E-ST-70-41C Section B.1 page 617.
366
367        This is the slow version. See metopsgizer source for faster implementations.
368        """
369        # this is the super-slow implementation
370        result = 0xffff
371        for b in self.buff[0:self.length - 2]:
372            for _ in range(8):
373                if ((b & 0x80) ^ ((result & 0x8000) >> 8)) != 0:
374                    result = ((result << 1) ^ 0x1021) & 0xffff
375
376                else:
377                    result = (result << 1) & 0xffff
378
379                b <<= 1
380
381        return result
382
383    @property
384    def time_status(self):
385        """Get Time Status value."""
386        if len(self.buff) < 17:
387            # raise ValueError('Attempt to read time_status from CCSDS packet of length {l}'.format(
388                # l=len(self.buff)))
389            return None
390
391        # S3, APID = 29 is HPTM packet with special format - return time status of 1
392        return self.buff[17] if self.apid != APID_HPTM else 1
393
394    @property
395    def pec(self):
396        """Get Packet Error Code."""
397        if not self.has_pec():
398            return None
399
400        pos = self.length - 2
401        # logger.debug('Reading PEC from {pos}'.format(pos=pos))
402        return unpack_uint16_be(self.buff, pos)
403
404    @property
405    def param1(self):
406        """Get Param1 value, or None if not defined for this packet."""
407        if CCSDSTM.packet_identifier is None:
408            # This is bad design and we shouldn't be altering a static member of CCSDSTM
409            # here. It should have been removed with the change to multi-satellite
410            # PUS support
411            # !!!!! todo, see previous comment. Still needs fixing. Problem is even worse now
412            # as we're passing a SID around. This is not good enough
413            CCSDSTM.packet_identifier = PacketCriteria(
414                CCSDSTM.sid.db_dir(settings.PARAM1_PARAM2_INFO_FILENAME))
415
416        param_source = CCSDSTM.packet_identifier.get_criteria(
417            self.service, self.subservice, self.apid)
418
419        if param_source is None:
420            # None would be more accurate, but SRDB uses 0 to denote no param1
421            return 0
422
423        offset = param_source.param1_offset
424        width = param_source.param1_width
425
426        if offset is None:
427            return 0
428
429        if offset+(width//8) > self.length:
430            raise CCSDSBadHeader(
431                'Manual param1 of apid {apid} service {service} subservice {subservice}'.format(
432                    apid=self.apid, service=self.service, subservice=self.subservice),
433                self.length,
434                offset+(width//8))
435
436        if width == 8:
437            return self.buff[offset]
438
439        elif width == 16:
440            return unpack_uint16_be(self.buff, offset)
441
442        elif width == 22:
443            # we assume that we take the 22 MSB
444            return unpack_uint22_be(self.buff, offset)
445
446        elif width == 24:
447            return unpack_uint24_be(self.buff, offset)
448
449        elif width == 32:
450            return unpack_uint32_be(self.buff, offset)
451
452        else:
453            raise ConfigError('Cannot read param1 with width {w}'.format(w=width))
454
455    @property
456    def param2(self):
457        """Get Param2 value, or None if not defined for this packet."""
458        param_source = CCSDSTM.packet_identifier.get_criteria(
459            self.service, self.subservice, self.apid)
460
461        if param_source is None:
462            return 0
463
464        offset = param_source.param2_offset
465        width = param_source.param2_width
466
467        if offset is None:
468            return 0
469
470        if offset + width//8 > self.length:
471            raise ValueError('Requested param2 offset of {o} is greater than packet length {l} in apid {a}|{s}|{ss}'.format(
472                o=offset, l=self.length, a=self.apid, s=self.service, ss=self.subservice))
473
474        if width == 8:
475            return self.buff[offset]
476
477        elif width == 16:
478            return unpack_uint16_be(self.buff, offset)
479
480        elif width == 22:
481            # we assume that we take the 22 MSB
482            return unpack_uint22_be(self.buff, offset)
483
484        elif width == 24:
485            return unpack_uint24_be(self.buff, offset)
486
487        elif width == 32:
488            return unpack_uint32_be(self.buff, offset)
489
490        else:
491            raise ConfigError('Cannot read param2 with width {w}'.format(w=width))
492
493    def criteria(self):
494        return self.service, self.subservice, self.apid, self.param1, self.param2
495
496    def show_header(self, target=sys.stdout, indent=''):
497        """Display basic packet header information to stdout."""
498        super(CCSDSTM, self).show_header(target, indent)
499        target.write("""{i}OBT: {obt}
500{i}Timestatus: {timestatus}
501{i}Param1: {param1}
502{i}Param2: {param2}
503""".format(i=indent,
504           timestatus=self.time_status,
505           obt=('/'.join(str(o) for o in self.obt) if self.obt is not None else 'none'),
506           param1=self.param1,
507           param2=self.param2))
508
509    def reset_sensing_time(self, sensing_time):
510        """Reset Sensing Time.
511            # From R Letor email 05-05-20
512            # it is to be noted that only for packets of the following type/sub-types: TM(191,26), TM(191,27), and TM(191,28),
513            # parameter COLLECTION_DATE should be used instead of the TM packet OBT as reference time for the timing of the
514            # super-commutated samples.
515            # For super-commutated parameters from any other TM packets, the packet OBT shall be used as reference time.
516            # A special rule shall therefore be put in place for the timing of the super-commutated parameters of
517            # PUS type 191, sub-types 26, 27 and 28 (DIAG, HRDM, and DHRD TM respectively).
518            # In that case, COLLECTION_DATE is a CUC time encoded on 4+3Bytes, starting from Byte 22 (counting up from 0 at
519            # start of the CCSDS TM packet). It is easier to address it which a fixed position rather than by its parameter
520            # name or position as name can vary and position (e.g calling it '2nd parameter') can also change if
521            # some intermediate overlapping parameters are defined in the MIB.
522        """
523        if self.dfh_flag == 0:
524            # not applicable
525            return sensing_time
526
527        if SID.special_HR_packets is not None and\
528           self.service in SID.special_HR_packets.hr_service and\
529           self.subservice in SID.special_HR_packets.hr_subservice:
530
531            # reset sensing time based on description above
532            byte_pos = SID.special_HR_packets.hr_time_location
533
534            # time component lengths in bytes
535            coarse_time_len = 4
536            fine_time_len = 3
537            length = coarse_time_len + fine_time_len
538
539            # convert datetime buffer to HEX Strings
540            raw_coarse = self.buff[byte_pos : byte_pos+coarse_time_len].hex()
541            raw_fine = self.buff[byte_pos+coarse_time_len : byte_pos+length].hex()
542
543            # convert to seconds:
544            #  first seconds
545            coarse = int(raw_coarse, 16)
546
547            # fraction of seconds
548            fine_prop=int(raw_fine, 16) / float(2**(fine_time_len*8))
549
550            seconds = coarse + fine_prop
551
552            # get original packet sensing time
553            utc_sensing_time = sensing_time
554
555            # get reference OBT
556            ref_obt = self.obt
557            ref_obt_s = ref_obt[0]
558            ref_obt_us = ((ref_obt[1] << 8) + ref_obt[2]) * (1e6 / (1 << 24))
559
560            # MTG - Use packet sensing time and packet header OBT as reference.
561            # So, param value is Ref time + param_OBT - header_OBT
562            reset_sensing_time = utc_sensing_time + \
563                                    timedelta(seconds=seconds) - \
564                                    timedelta(seconds=ref_obt_s, microseconds=ref_obt_us)
565
566        else:
567            # return unmodified sensing time
568            reset_sensing_time = sensing_time
569
570        return reset_sensing_time
571
572
573class CCSDS_NO_HDR(CCSDS):
574    """CCSDS packet with no defined header. `buffer` can be much bigger than the packet.
575
576    if packet has a NIS header then, for some reason TBC, ccsds header not reliable
577    and sequence_count is ccsds.buff[3]
578    """
579    HEADER_SKIP = 0
580
581    def payload(self):
582        return self.buff[self.HEADER_SKIP:]
583
584    def write(self, handle):
585        """We don't write these packets to file from ccsds_tool --output option."""
586        return 0
587
588    @property
589    def version_number(self):
590        """Get Version Number (=0)."""
591        return 0
592
593    @property
594    def type_flag(self):
595        """Get Type Flag."""
596        return 0
597
598    @property
599    def dfh_flag(self):
600        """Get DFH Flag."""
601        return 0
602
603    @property
604    def apid(self):
605        """Get Application ID - not set reliably."""
606        return 
607
608    @property
609    def grouping_flags(self):
610        """Get Sequence Grouping Flags."""
611        return 0
612
613    @property
614    def sequence_count(self):
615        """Get Sequence Count - only applicable to good Time Frame packets."""
616        if len(self.buff) > 3:
617            return self.buff[3]
618        else:
619            return 0
620
621    @property
622    def dflength(self):
623        """Retrieve the Data Field Length."""
624        try:
625            return unpack_uint16_be(self.buff, 4)
626        except IndexError:
627            raise CCSDSBadHeader('dflength', len(self.buff), 8)
628
629    @property
630    def destination_id(self):
631        """Get Destination ID - not set reliably."""
632        return 0
633
634    @property
635    def obt_single(self):
636        """Get OBT time - not set reliably."""
637        return 0
638
639    @property
640    def time_status(self):
641        """Get Time Status value."""
642        return None
643
644    def show_header(self, target=sys.stdout, indent=''):
645        """Display basic packet header information to stdout."""
646        target.write("""{i}Version: {version}
647{i}Type: {type}
648{i}DFH flag: {dfh}
649{i}Group flags: {group}
650{i}Sequence count: {count}
651{i}Length: {dflength}
652""".format(
653    i=indent,
654    version=self.version_number,
655    type=self.type_flag,
656    dfh=self.dfh_flag,
657    group=self.grouping_flags,
658    count=self.sequence_count,
659    dflength=len(self.buff)
660))
661
662
663class CCSDS_NIS_NO_HDR(CCSDS_NO_HDR): 
664    """TM ccsds record with NIS header, so with no CCSDS Header.
665    if packet has a NIS header then, for some reason TBC, ccsds header not reliable
666    and sequence_count is ccsds.buff[3]
667    """
668    pass
669
670
671class CCSDSEV(CCSDS):
672    # No CCSDS header  Not completed  or correct TBD
673
674    packet_identifier = None
675
676    @property
677    def dfh_flag(self):
678        """Get DFH Flag."""
679        return 0
680
681    @property
682    def length(self):
683        """Compute correct checksum for this packet. Not Applicable for EV Packet"""
684        # Rapid_header_len + TDEV_header_Length = 120
685        return len(self.buff) - 120
686
687    @property
688    def apid(self):
689        """Apid for this packet. Not Applicable for EV Packet"""
690        return 0
691
692
693    def checksum(self):
694        """Compute correct checksum for this packet. Not Applicable for EV Packet"""
695        return None
696
697    def criteria(self):
698        # !!! Investigate, document and fix this !!!
699        return 'NA'
700
701    def write(self, handle):
702        """Store the (possibly modified) CCSDS packet to `handle` as a raw packet,no header."""
703
704        length = self.length
705        handle.write(self.buff[:length])
706        return length
707
708
709    def show_header(self, target=sys.stdout, indent=''):
710        """Display basic packet header information to stdout."""
711        target.write("""{i}Event Message:
712""".format(i=indent))
713
714
715# S3-ID-TAF-SC-01754_-_IF1_Packet_Structure_ICD-Iss6
716# LAYOUT = """
717# This diagram shows the MetOp CCSDS layout, the record structure contained in the EDDS .xml file.
718# It does not fully represent S3 CCSDS packets.
719
720# +-----------------------------------------------------+------------------------------------------+
721# |                 PACKET PRIMARY HEADER               |              PACKET DATA FIELD           |
722# |                       (6 Bytes)                     |              (Max:65536 Bytes)           |
723# +----------------------------+--------------+---------+---------+--------------------------------+
724# |   Packet Identification    |Packet Seq.   |Packet   |Secondary|         Source Data            |
725# |                            |Control       |Length   |Header   +---------------------------+----+
726# |                            |              |         |         |          User Data        |PEC |
727# |         (2 bytes)          |(2 bytes)     |(2 bytes)|(8 bytes)|                           |    |
728# +----------------------------+------+-------+-------------------+---------------------------+----+
729# |Version|Type |Sec.  |APID   |Seq.  |Seq.   |         |UTC-Time |   Ancillary   |Application|    |
730# |Number |Ind. |Header|       |Flags |Counter|         |Stamp    |     Data      |   Data    |    |
731# |       |     |Flag  |       |      |       |         |         +----+----------|           |    |
732# |       |     |      |       |      |       |         |         |OBT |Ancillary |           |    |
733# |       |     |      |       |      |       |         |         |    |Info Field|           |    |
734# |3 bits |1 bit|1 bit |11 bits|2 bits|14 bits|         |         |    |          |           |    |
735# +-------+-----+------+-------+------+-------+---------+---------+----+----------+-----------+----+
736# |          16 bits           |   16 bits    | 16 bits | 64 bits | 48 | Variable | Variable  | 16 |
737# |                            |              |         |         |bits|          |           |bits|
738# +----------------------------+--------------+---------+---------+----+----------+-----------+----+
739# """
740
741# """
742# +--------------------------------------------------------------------------------------------------+
743# | TM Packet (min 20 bytes bytes)                                                                   |
744# +--------------------------------------------------------------------------------------------------+
745# | TM Packet Header                | Packet Data Field                                              |
746# |                                 +----------------------------------------------------------------+
747# |                                 | Data Field Header                      |Source|Packet          |
748# |                                 |                                        |Data  |Error           |
749# |                                 |                                        |      |Control         |
750# +---------------------------------+----------------------------------------+------+----------------+
751# |Ver|Typ|DHF|APID   |Packet   |Pkt|Spare|PUS|Spare|Srv|Srv|Dest|Time|Time  |      |
752# |Num|Flg|Flg|       |Sequence |DF |     |Ver|     |Typ|Sub|ID  |    |status|      |
753# |   |   |   |       |Control  |Len|     |Num|     |   |Typ
754
755# |   |   |   |PID|CAT|Group|Src|   |     |
756# |   |   |   |       |Flags|Seq|   |     |
757# |                         |Cnt|
758
759
760# """
761
762# We need this due to special format of HPTM packet
763# SERVICE_HPTM = 0
764# SUBSERVICE_HPTM = 0
765# APID_HPTM = 29
766
767
768class CCSDSContainer(ABC):
769    """Represent an object that contains a CCSDS packet, such as a raw byte array or RAPID block."""
770
771    def __init__(self):
772        self.tdev_tc_header = None
773
774    @abstractmethod
775    def packet_def(self):
776        """Return the PacketDef for reading the contained packet."""
777        pass
778
779    # Need some weird support code to make this work. It's not vital though. Just remember
780    # to write sensing_time property on any derived classes
781    # @abstractmethod
782    # @property
783    # def sensing_time(self):
784        # """Read or compute the CCSDS packet sensing time as a `datetime`."""
785        # pass
786
787    # @abstractmethod
788    # @property
789    # def ccsds(self):
790        # """Our container CCSDS packet, or None if we don't."""
791        # pass