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()