1#!/usr/bin/env python3
  2
  3from collections import namedtuple
  4from collections import defaultdict
  5from datetime import timedelta
  6from datetime import datetime
  7from enum import Enum
  8from typing import Union
  9from typing import Callable
 10
 11from chart.common.xml import SubElement
 12from chart.plots.sampling import Sampling
 13from chart.common.xml import parsechildstr
 14from chart.sids.exceptions import NoSuchSID
 15from chart.sids import SIDBase
 16from chart.common.decorators import memoized
 17from chart.common.decorators import memoized2
 18from chart.common.xml import XMLElement
 19from chart.project import settings
 20
 21ELEM_SATELLITES = 'satellites'
 22ELEM_SATELLITE = 'satellite'
 23ELEM_NAME = 'name'
 24ELEM_DESCRIPTION = 'description'
 25ELEM_LAUNCH_DATE = 'launch-date'
 26ELEM_DECOMMISION_DATE = 'decommision-date'
 27ELEM_ORBIT_DURATION = 'orbit-duration'
 28ELEM_SIDS = 'sids'
 29ELEM_SID = 'sid'
 30ELEM_SID_NUM = 'sid-num'
 31ELEM_LONG_NAME = 'long-name'
 32ELEM_COLOUR = 'colour'
 33ELEM_VISIBLE = 'visible'
 34ELEM_OPERATIONAL = 'operational'
 35ELEM_GROUP = 'group'
 36ELEM_ORBITER = 'orbiter'
 37ELEM_GOOD_FRAME_SPID = 'good-frame-spid'
 38ELEM_BAD_FRAME_SPID = 'bad-frame-spid'
 39ELEM_UNKNOWN_PACKET_SPID = 'unknown-packet-spid'
 40ELEM_BAD_PACKET_SPID = 'bad-packet-spid'
 41ELEM_IDLE_PACKET_SPID = 'idle-packet-spid'
 42ELEM_IDLE_FRAME_SPID = 'idle-frame-spid'
 43ELEM_SPECIAL_PACKETS = 'special-packets'
 44
 45class Satellite:
 46    def __init__(self,
 47                 name,
 48                 description=None,
 49                 launch_date=None,
 50                 decommision_date=None,
 51                 orbit_duration=None,
 52                 ground_test=None):
 53        self.name = name
 54        self.description = description
 55        self.launch_date = launch_date
 56        self.decommision_date = decommision_date
 57        self.orbit_duration = orbit_duration
 58        self.ground_test = ground_test
 59
 60
 61@memoized
 62def all_satellites(satellites_elem):
 63    """Parse <satellites> from sattings.SOURCES and return a list of Satellites."""
 64    result = []
 65    for sat_elem in satellites_elem.findall(ELEM_SATELLITE):
 66        result.append(Satellite(
 67            name=sat_elem.parse_str(ELEM_NAME),
 68            description=sat_elem.parse_str(ELEM_DESCRIPTION, None),
 69            launch_date=sat_elem.parse_datetime(ELEM_LAUNCH_DATE, None),
 70            decommision_date=sat_elem.parse_datetime(ELEM_DECOMMISION_DATE, None),
 71            orbit_duration=sat_elem.parse_timedelta(ELEM_ORBIT_DURATION, None)))
 72
 73    return result
 74
 75@memoized
 76def all_sids():
 77    """Parse <satellites> from settings.SOURCES and return a list of SIDs."""
 78    SID_JCS.construction_started = True
 79    # print('all_sids')
 80    root_elem = XMLElement(filename=settings.SOURCES)
 81    sids_elem = root_elem.find(ELEM_SIDS)
 82    satellites_elem = root_elem.find(ELEM_SATELLITES)
 83    result = []
 84    for sid_elem in sids_elem.findall(ELEM_SID):
 85        name = sid_elem.parse_str(ELEM_NAME)
 86        satellite_s = sid_elem.parse_str(ELEM_SATELLITE)
 87        satellite = None
 88        for s in all_satellites(satellites_elem):
 89            if s.name == satellite_s:
 90                satellite = s
 91
 92        if satellite is None:
 93            raise ValueError('SID {n} has no satellite found'.format(n=name))
 94
 95        result.append(SID_JCS(
 96            sid_num=sid_elem.parse_int(ELEM_SID_NUM),
 97            name=name,
 98            long_name=sid_elem.parse_str(ELEM_LONG_NAME, None),
 99            description=sid_elem.parse_str(ELEM_DESCRIPTION, None),
100            satellite=satellite,
101            colour=sid_elem.parse_str(ELEM_COLOUR, None),
102            operational=sid_elem.parse_bool(ELEM_OPERATIONAL, True),
103            visible=sid_elem.parse_bool(ELEM_VISIBLE, True),
104            group=sid_elem.parse_str(ELEM_GROUP, None),
105            orbit=sid_elem.parse_str(ELEM_ORBITER, None)))
106
107    # now fix the orbit members to point to an instance of the orbit<>timestamp converter class
108    for sid in result:
109        # check for sids that use another sid's orbiter
110        for s in result:
111            if sid.orbit == s.name:
112                sid.orbit = s.orbit
113
114        orbiter = s.orbit
115
116        # otherwise the SID should use it's named class as the orbit determiner
117        # i'm being lazy import the class properly here
118        if sid.orbit == 'chart.common.orbits_geo_events.OrbitDeterminer':
119            from chart.products.fdf.orbits_geo_events import OrbitDeterminer
120            sid.orbit = OrbitDeterminer(sid)
121
122    # read special packets definitions
123    SID_JCS.special_packets = SpecialPackets(root_elem.find(ELEM_SPECIAL_PACKETS))
124
125    SID_JCS.construction_allowed = False
126
127    # Used to represent a report which is has no source, usually a system
128    # job like purge or a system digest report
129    # uses wildcard to switch off the validity check
130    # SID_JCS.SIDLESS = SID_JCS(name='SYS', sidless=True)
131    result.append(SID_JCS(sidless=True))
132
133    return result
134
135
136class SID_JCSMeta(type):
137    """Cached constructor.
138
139    During application startup, SID_JCS.construction_allowed is set and new instances
140    of SID_JCS can be freely created and added to the SID_JCS.all_sids list.
141
142    Later, `construction_allowed` is cleared and no new SIDs can be created, only retrieved
143    from the existing list.
144    """
145
146    def __call__(cls, name=None, sid_num=None, sidless=False, *args, **kwargs):
147        # print('metasid',cls,'name',name,'sid_num',sid_num,'args',args,'kwargs',kwargs)
148
149        if not SID_JCS.construction_started:
150            all_sids()
151
152        if SID_JCS.construction_allowed or sidless:
153            # allow sids from file and sidless to be actualy constructed
154            res = super(SID_JCSMeta, cls).__call__(name=name, sid_num=sid_num, *args, **kwargs)
155            return res
156
157        # SID_JCS.construction_allowed = False
158        for sid in all_sids():
159            # print('testing ',sid_num,name,' against ',sid)
160            if sid.sid_num == sid_num or (
161                    sid.name is not None and name is not None and sid.name.upper() == name.upper()):
162                # print('returning ', sid)
163                return sid
164
165        # print('none found')
166
167
168class SpecialPackets:
169    """Mission-specific PUS RAPIDFILE SPID values that have special meanings in decoding code."""
170
171    def __init__(self, elem):
172        self.good_frame_spid = elem.parse_int(ELEM_GOOD_FRAME_SPID)
173        self.bad_frame_spid = elem.parse_int(ELEM_BAD_FRAME_SPID)
174        self.unknown_packet_spid = elem.parse_int(ELEM_UNKNOWN_PACKET_SPID)
175        self.bad_packet_spid = elem.parse_int(ELEM_BAD_PACKET_SPID)
176        self.idle_packet_spid = elem.parse_int(ELEM_IDLE_PACKET_SPID)
177        self.idle_frame_spid = elem.parse_int(ELEM_IDLE_FRAME_SPID)
178
179
180class SID_JCS(SIDBase, metaclass=SID_JCSMeta):  # , metaclass=sid_jcs_meta):
181    """JCS Source ID object. To become general PUS or non-PUS sid_num based source.
182
183    For any SID identified by a simple name i.e. only SID_MSG is not suitable for folding
184    into this object."""
185
186    selector = 'jcs'
187    default_sid = {'sid': 'JCSA'}
188    # all_sids = []
189
190    # these objects are only created during startup when settings.SOURCES is parsed. Afterward
191    # clients can only create SIDs via the metaclass cached lookup
192    construction_allowed = True
193    construction_started = False
194
195    sampling_options = (Sampling.AUTO,
196                        Sampling.ALL_POINTS,
197                        Sampling.FIT,
198                        Sampling.ORBITAL)
199                        # Sampling.DAILY,
200                        # Sampling.DAILY_TOTALS)
201
202    special_packets = None
203    # class SpecialPackets(Enum):
204        # GOOD_FRAME_SPID = 90010
205        # BAD_FRAME_SPID = 90020
206        # UNKNOWN_PACKET_SPID = 90030
207        # BAD_PACKET_SPID = 90040
208        # IDLE_PACKET_SPID = 90050
209        # IDLE_FRAME_SPID = 90060
210
211    # After reading the SPID from a RapidFile block, we determine it's a CADU block
212    # if it matches any of these values
213    # CADU_RECORDS = [
214        # SpecialPackets.GOOD_FRAME_SPID,
215        # SpecialPackets.BAD_FRAME_SPID,
216        # SpecialPackets.UNKNOWN_PACKET_SPID,
217        # SpecialPackets.BAD_PACKET_SPID,
218        # SpecialPackets.IDLE_PACKET_SPID,
219        # SpecialPackets.IDLE_FRAME_SPID,
220    # ]
221
222    # With NIS header, i.e. Frames
223    # self.special_packets.WITH_NIS_HEADER = [
224        # self.special_packets.GOOD_FRAME_SPID,
225        # self.special_packets.BAD_FRAME_SPID,
226        # self.special_packets.IDLE_FRAME_SPID,
227    # ]
228
229        # Without NIS Header, i.e. Packets
230        # self.special_packets.NO_NIS_HEADER = [
231            # self.special_packets.UNKNOWN_PACKET_SPID,
232            # self.special_packets.BAD_PACKET_SPID,
233            # self.special_packets.IDLE_PACKET_SPID,
234        # ]
235
236    def __init__(self,
237                 sid_num: int=None,
238                 name: str=None,
239                 long_name: str=None,
240                 description: str=None,
241                 satellite: Satellite=None,
242                 colour: str=None,
243                 operational: bool=True,
244                 visible: bool=True,
245                 default: bool=False,
246                 orbit: Union[str, Callable]=None,
247                 group=None):
248        """Args:
249
250            `sid_num`: Unique SID_NUM entry for database storage
251            `name`: Normal short unique name for this source
252            `long_name`: Longer and more descriptive name for example for graph titles
253            `description`: Very long name. Not currently used.
254            `satellite`: The Satellite object this SID refers to. Multiple SIDs can refer
255                to the same satellite.
256            `colour`: Suggested rendering colour for this SID used in the report viewer
257            `operational`: The SID is currently potentially in-flight and generating data.
258                Used to determine if regular algorithms and reports should be generated.
259            `visible`: Allow this SID to be hidden from the user interface, for internal testing
260                SIDs
261            `default`: True if this is the normal SID the user interface selects by default
262            `orbiter`: A callable (fn or class) used to map orbit numbers to times
263            `group`: Allow related SIDs to be grouped together in a user interface
264        """
265        # print('sid init ' + str(name))
266        self.sid_num = sid_num
267        self.name = name
268        self.long_name = long_name
269        self.description = description
270        self.satellite = satellite
271        self.colour = colour
272        self.operational = operational
273        self.visible = visible
274        self.default = default
275        self.orbit = orbit
276        self.group = group
277
278    def __str__(self):
279        """Used for plotviewer titles and report viewer calendar."""
280        if self.name is None:
281            return 'Non satellite specific'
282
283        else:
284            return self.name
285
286    @staticmethod
287    def ddl_id_fields():
288        """SID fields implicitly added to the start of all TS tables."""
289        from chart.db.model.table import FieldInfo
290        return [
291            FieldInfo(name='SID_NUM',
292                      description='Source ID number',
293                      datatype=int,
294                      length=8),
295        ]
296
297    @staticmethod
298    def all(operational=None, visible=None):
299        """Yield all satellites matching parameters."""
300        for s in all_sids():
301            if (operational is None or operational == s.operational) and\
302               (visible is None or visible == s.visible):
303                yield s
304
305    @staticmethod
306    def sql_sys_select(table_name):  # (unused arg) pylint: disable=W0613
307        """Craft SQL fragment to retrieve the fields of a SID from a system table.
308
309        SID is constructed later from_sys_select()."""
310
311        return ['SID_NUM']
312
313    def to_xml(self, elem):
314        """Annotate parent `elem` with ourselves, creating child nodes."""
315        if self.name is not None:
316            SubElement(elem, ELEM_SID).text = self.name
317
318    @staticmethod
319    def from_xml(elem, wildcard=False):
320        """Create a SID from childs of `elem`, or None if not found.."""
321        # First check for <sid>NAME</sid>
322        sid = parsechildstr(elem, ELEM_SID, None)
323        if sid is not None:
324            return SID_JCS(sid)
325
326        # Now look for <sid-num>X</sid-num>
327        # I'm not sure this happens
328        sid_num = parsechildstr(elem, ELEM_SID_NUM, None)
329        if sid_num is not None:
330            return SID_JCS(sid_num=sid_num)
331
332        # Now look for <name>NAME</name>
333        # I really don't think this does or should happen
334        name = parsechildstr(elem, ELEM_NAME, None)
335        if name is not None:
336            return SID_JCS(name=name)
337
338        return None
339
340    @staticmethod
341    def django_all():
342        """Return SID information sent to the web client."""
343        return [sid.as_dict() for sid in SID_JCS.all(operational=None)]
344
345    def as_dict(self):
346        """Convert ourselves into a dictionary of data for the web interface client."""
347        # operational = self.satellite is not None and self.satellite.operational
348        return {'menu_value': {'sid': self.name},
349                'initial_year': None,  # self.satellite.launch_date.year
350                                # if self.satellite.launch_date is not None else None,
351                # 'menu_name': '{name} ({scid})'.format(
352                    # scid=self.scid,
353                    # name=self.scid),  # satellite.name),
354                'menu_name': self.name,
355                'time_offset': 0, # self.satellite.time_offset.total_seconds()
356                        # if self.satellite is not None and self.satellite.time_offset is not None
357                        # else 0,
358                'title': self.long_name,  # for plot viewer title
359                'operational': True,  # operational
360                }
361
362    @staticmethod
363    def ts_indexes():
364        """Return list of TS AP table field names which shuold be indexed."""
365        return ['SID_NUM', ]
366
367    # @staticmethod
368    # def ts_fields():
369        # """Return a list of field names needed to store our instances in timeseries tables."""
370        # return ('SID_NUM', )
371
372    # def ts_values(self):
373        # """Return a list of values to store this specific instance in a timeseries table."""
374        # return [self.sid_num]
375
376    insert_fields = ['SID_NUM']
377
378    def bind_insert(self):
379        return [self.sid_num]
380
381    @staticmethod
382    def from_django_request(request):
383        """Extract SID(s) from `query`, either a Django request GET object
384        or a dictionary or extracted pairs from a plot URL datapoint (dict)."""
385        sid = request.get('sid')
386        if sid is None:
387            return None
388
389        if sid == 'SYS':
390            result = SID_JCS()
391            # return SID_JCS.SIDLESS
392
393        result = SID_JCS(name=sid)
394        assert result.sid_num is not None
395        return result
396
397    @staticmethod
398    def from_django_params(params, remove=False):
399        """Extract a SID from `query` (URL parameter string)."""
400        if 'sid' in params:
401            res = SID_JCS(params['sid'])
402            if remove:
403                del params['sid']
404
405            return res
406
407        else:
408            return None
409
410    def as_url_query(self, base=None):
411        """Return a dictionary representation of ourselves suitable for passing into
412        the urllib.urlencode() function to create/extend a URL query fragment."""
413        if base is None:
414            if self.name is None:
415                return {'sid': 'SYS'}
416
417            else:
418                return {'sid': self.name}
419
420        else:
421            if self.name is None:
422                base['sid'] = 'SYS'
423
424            else:
425                base['sid'] = self.name
426
427            return base
428
429    @staticmethod
430    def from_string(st):
431        for sid in SID_JCS.all():
432            if sid.name is not None and st.lower() == sid.name.lower():
433                return sid
434
435        raise ValueError()
436
437    def expand(self):
438        """Wildcard support, probably not needed but this still gets called."""
439        return [self]
440
441    @staticmethod
442    def get_reportname_part(sid):
443        """Return the SID fragment of a report name e.g. the M02 in
444        SVM_DHSA_REPORT_M02_20130901.zip.
445        Also used as subdirectory name in the reports archive.
446        Also used as part of the statefiles name.
447        """
448        if sid is None or sid.name is None:
449            return 'SYS'
450
451        else:
452            return sid.name
453
454    # subsampling options to be considered when auto-selecting best stats subsampling
455    # should be sorted from longest to shortest
456    def auto_sampling_options(self):
457        """Return the list of stats subsampling options available for this sid.
458        """
459        return [Sampling.ORBITAL]
460        # return [Sampling.FIT]  \ fails as plot tool not handle
461
462    def sql_where(self, table_name=None, match_on_none=False, prefix=None):
463        """Return an SQL clause that will test for ts rows containing us.
464
465        If `table_name` is set it will be the name (not Tableinfo) of the table to be written
466        to, which may be a system table.
467
468        If `match_on_none` is set then we will match table rows contains null values in the SID
469        column.
470
471        `prefix` can be used to prefix column names for queries that join tables.
472
473        Avoids bind variables."""
474        # if self.sid_num is None:
475            # we are the wildcard SID that matches anything
476            # return '1=1'
477
478        if match_on_none:
479            return '(SID_NUM is null or SID_NUM={sidnum})'.format(sidnum=self.sid_num)
480
481        else:
482            return 'SID_NUM={sidnum}'.format(sidnum=self.sid_num)
483
484    @staticmethod
485    def from_reportname_part(fragment):
486        """Decode a report name fragment."""
487        if fragment == 'SYS':  # for system reports like digest
488            return SID_JCS()
489
490        else:
491            return SID_JCS(fragment)
492
493    @staticmethod
494    def all_report_sids():
495        """Scan REPORTs table and return a dictionary of report name against a set of
496        SIDs for which we have reports of that activity."""
497        # only used by reportviewer. can be removed
498        from chart.db.connection import db_connect
499        res = defaultdict(list)
500        db_conn = db_connect('REPORTS')
501        for activity, sid_num in db_conn.query(
502                'SELECT DISTINCT activity, sid_num FROM reports ORDER BY sid_num'):
503            res[activity].append(SID_JCS(sid_num=sid_num) if sid_num is not None else SID_JCS())
504
505        return res
506
507    @staticmethod
508    def ddl_sys_fields(table_name=None):  # (unused arg) pylint: disable=W0613
509        """Events, jobs, reports, subscriptions tables using the {{sid}} expansion."""
510        from chart.db.model.table import FieldInfo
511        return [
512            FieldInfo(name='SID_NUM',
513                      datatype=int,
514                      length=4,
515                      allow_null=True,
516                      description='Spacecraft ID'),
517            ]
518
519    def cal_dirname(self):
520        """Compute a subdirectory to write named calibration files to."""
521        return self.short_name
522
523    def cal_name(self):
524        """Compute a suitable plSQL function name suffix."""
525        return self.name
526
527    @property
528    def short_name(self):
529        return self.name
530
531    @staticmethod
532    def sql_sys_where(table_name, sid):  # (unused arg) pylint: disable=W0613
533        """Return an SQL clause which will test for us in the JOBS or REPORTS or EVENTS table.
534        If `sid` is None then don't apply any filtering."""
535        # should be removed and reported with all bind variables
536        if sid is None:
537            return '1=1'
538
539        else:
540            return "SID_NUM='{sidnum}'".format(sidnum=sid.sid_num)
541
542    @staticmethod
543    def sql_sys_where_bind(table_name):  # (unused arg) pylint: disable=W0613
544        """SQL fragment to use in WHERE clauses filtering for us, using bind variables."""
545        return 'SID_NUM=:sid_num'
546
547    def bind_sys_where(self, table_name):  # (unused arg) pylint: disable=W0613
548        """Convert ourselves into a bindvars dict for use with the sql emitted by
549        sql_sys_where_bind()."""
550        return {'sid_num': self.sid_num}
551
552    @staticmethod
553    def sql_sys_insert(table_name):
554        """Return 2 SQL fragments for an insert statement:
555        - Fields
556        - Bind variables
557
558        E.g.:
559                sid_vars, sid_binds = SID.sql_sys_insert('JOBS')
560        insert_job.ins_cur = db_conn.prepared_cursor(
561            'INSERT INTO JOBS (CATEGORY, ACTIVITY, FILENAME, DIRNAME, {sidfield}ORBIT, '
562            'SENSING_START, SENSING_STOP, TABLENAME, EARLIEST_EXECUTION_TIME '
563            'VALUES '
564            '(:category, :activity, :filename, :dirname, {sidbind}:orbit, :sensing_start, '
565            ':sensing_stop, :tablename, earliest_execution_time'.format(
566                sidfield=sid_fields, sidbind=sid_binds))
567        .
568        """
569        return (',SID_NUM', ',:sidnum')
570
571    @staticmethod
572    def sql_sys_update(table_name):  # (unused arg) pylint:disable=W0613
573        """SQL fragment, using bind variables, to update the SID part of a system table."""
574        return ',sid_num=:sidnum'
575
576    @staticmethod
577    def bind_sys_insert(table_name, sid):
578        """Convert ourselves into a dict to be inserted into an insert statement variable
579        list.
580        usage:
581    db_conn.query('INSERT INTO PRODUCTS (activity, filename, result{sidfields}, sensing_start) '
582             'VALUES (:activity, :filename, :result{sidbinds}, :sensing_start)'.format(
583                 sidfields=sid_fields, sidbinds=sid_binds),
584                  activity=activity,
585                  filename=filename,
586                  result=result,
587                  sensing_start=sensing_start,
588                  **SID.bind_sys_insert('PRODUCTS', sid))
589        .
590        """
591        if sid is None:
592            return {'sidnum': None}
593
594        else:
595            return {'sidnum': sid.sid_num}
596
597    @staticmethod
598    def from_sys_select(table_name, args):  # (unused arg) pylint: disable=W0613
599        """Construct a SID from the fields requested by sql_sys_select()."""
600        if args[0] is None:
601            return None
602
603        else:
604            return SID_JCS(sid_num=args[0])
605
606    @staticmethod
607    def report_sids(activity):
608        """Only used by reportviewer. can be removed."""
609        from chart.db.connection import db_connect
610        db_conn = db_connect('REPORTS')
611        res = []
612        for activity, sid_num in db_conn.query(
613                'SELECT DISTINCT activity, sid_num FROM reports WHERE activity=:reportname',
614                reportname=activity.name):
615            res.append(SID_JCS(sid_num=sid_num))
616
617        return res
618
619    @memoized2
620    def all_tables(self):
621        """Yield all timeseries tables."""
622        from chart.db.model.table import TableInfo
623        return list(TableInfo.all())
624
625
626#SID_JCS.SIDLESS = SID_JCS()
627
628#class SIDManager_JCS(SIDManagerBase):
629#    def __init__(self):
630#        super(self, SIDManager_JCS).__init__(SID_JCS)
631
632# SAT_JCSA = Satellite(name='JCSA',
633#                      launch_date=datetime(2019, 1, 1),
634#                      orbit_duration=timedelta(seconds=6081))
635
636# SAT_JCSB = Satellite(name='JCSB',
637#                      launch_date=datetime(2022, 1, 1),
638#                      orbit_duration=timedelta(seconds=6081))
639
640# SID_JCS.all_sids = [
641#     SID_JCS(sid_num=1,
642#             scid='JCSA',
643#             name='JCS-A Operational',
644#             satellite=SAT_JCSA,
645#             colour='#2E5CB8',
646#             visible=True),
647#     SID_JCS(sid_num=2,
648#             scid='JCSB',
649#             name='JCS-B Operational',
650#             satellite=SAT_JCSB,
651#             colour='#2E5CB8',
652#             visible=True),
653#     SID_JCS(sid_num=3,
654#             scid='JCSA_VAL',
655#             name='JCS-A Validation GS',
656#             satellite=SAT_JCSA,
657#             colour='#2E5CB8',
658#             visible=True),
659#     SID_JCS(sid_num=4,
660#             scid='JCSA_IVV',
661#             name='JCS-A IV&V GS',
662#             satellite=SAT_JCSA,
663#             colour='#2E5CB8',
664#             visible=True),
665#     SID_JCS(sid_num=5,
666#             scid='JCSA_GND',
667#             name='JCS-A Ground Test Data',
668#             satellite=SAT_JCSA,
669#             colour='#2E5CB8',
670#             visible=True),
671#     SID_JCS(sid_num=6,
672#             scid='JCSA_SYNTH',
673#             name='JCS-A Synthetic Test Data',
674#             satellite=SAT_JCSA,
675#             colour='#2E5CB8',
676#             visible=True),
677#     SID_JCS(sid_num=7,
678#             scid='JCSA_TST',
679#             name='JCS-A Internal Testing Data',
680#             satellite=SAT_JCSA,
681#             colour='#2E5CB8',
682#             visible=False),
683# ]
684
685# SID_JCS.construction_allowed = False
686
687#iif __name__ == '__main__':
688#    from chart.common.args import ArgumentParser
689#    parser = ArgumentParser()
690#    args = parser.parse_args()
691#    print('Satellites')
692#    for s in all_satellites():
693#        print('  ', s.name)
694#    print('SIDs')
695#    for s in all_sids():
696#        print('  ', s.name)
697#    print('Testing JCSA', SID_JCS('JCSA'))
698
699# This is not the neatest because it means we're parsing the XML sources.xml on import
700# but the CCSDS ingestion code refers to SID.special_packets before any SID has been instantiated
701# all_sids()