space-packet-parser 4.0.2__tar.gz → 4.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  cff-version: 1.2.0
2
2
  title: 'space_packet_parser'
3
3
  type: software
4
- version: '4.0.2'
4
+ version: '4.2.0'
5
5
  description: A CCSDS telemetry packet decoding library based on the XTCE packet format description standard.
6
6
  license: BSD-3-Clause
7
7
  abstract: The space_packet_parser Python library is a generalized, configurable packet decoding library for CCSDS telemetry
@@ -11,9 +11,9 @@ abstract: The space_packet_parser Python library is a generalized, configurable
11
11
  generator pattern. The binary string may originate from an in-memory bit stream, a buffered file reader opened in
12
12
  binary mode, or a python socket object; in every case, a small buffer is used to read chunks of data to ensure that
13
13
  memory usage is minimal. The space_packet_parser library supports robust error handling and is capable of skipping malformed
14
- packet structures, unidentified packet structures, and can dynamically parse mixed APID packet streams. The
14
+ packet structures and unidentified packet structures and can dynamically parse mixed APID packet streams. The
15
15
  `Parser.generator` object supports keyword arguments for debugging such as the ability to return unraised parsing
16
- errors, which contain
16
+ errors, which contain partial instances of parsed packet data to aid in debugging.
17
17
  authors:
18
18
  - email: gavin.medley@lasp.colorado.edu
19
19
  name: Gavin Medley
@@ -21,9 +21,12 @@ authors:
21
21
  - email: michael.chambliss@lasp.colorado.edu
22
22
  name: Michael Chambliss
23
23
  orcid: "0009-0003-7493-0542"
24
+ - email: greg.lucas@lasp.colorado.edu
25
+ name: Greg Lucas
26
+ orcid: "0000-0003-1331-1863"
24
27
  maintainers:
25
28
  - email: gavin.medley@lasp.colorado.edu
26
29
  name: Gavin Medley
27
30
  orcid: "0000-0002-3520-9715"
28
31
  repository-code: "https://github.com/medley56/space_packet_parser"
29
- url: "TBD"
32
+ url: "https://space-packet-parser.readthedocs.io"
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.1
2
+ Name: space_packet_parser
3
+ Version: 4.2.0
4
+ Summary: A CCSDS telemetry packet decoding library based on the XTCE packet format description standard.
5
+ Home-page: https://space-packet-parser.readthedocs.io
6
+ License: BSD-3-Clause
7
+ Keywords: ccsds,xtce,space data systems,space packet protocol,packet parsing,lasp,university of colorado,data processing,data extraction,data manipulation,data transformation,data encoding,data decoding,packet inspection,binary data,python
8
+ Author: Gavin Medley
9
+ Author-email: gavin.medley@lasp.colorado.edu
10
+ Maintainer: Gavin Medley
11
+ Maintainer-email: gavin.medley@lasp.colorado.edu
12
+ Requires-Python: >=3.8
13
+ Classifier: License :: OSI Approved :: BSD License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Dist: bitstring (>=4.0.1)
21
+ Project-URL: Repository, https://github.com/medley56/space_packet_parser
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Space Packet Parser
25
+
26
+ ----------
27
+ [![Test Status](https://github.com/medley56/space_packet_parser/actions/workflows/tests.yml/badge.svg)](https://github.com/medley56/space_packet_parser/actions/workflows/tests.yml)
28
+ [![Doc Status](https://readthedocs.org/projects/space-packet-parser/badge/?version=latest)](https://readthedocs.org/projects/space-packet-parser/)
29
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7735001.svg)](https://doi.org/10.5281/zenodo.7735001)
30
+
31
+ Documentation: [https://space-packet-parser.readthedocs.io/en/latest/](https://space-packet-parser.readthedocs.io/en/latest/)
32
+
33
+ Space Packet Parser is a package for decoding CCSDS telemetry packets according to an XTCE or CSV packet structure definition.
34
+ It is based on the UML model of the XTCE spec and aims to support all but the most esoteric elements of the
35
+ XTCE telemetry packet specification.
36
+
37
+ Resources:
38
+ - [XTCE (Green Book - Informational Report)](https://public.ccsds.org/Pubs/660x2g2.pdf)
39
+ - [XTCE Element Description (Green Book - Informational Report)](https://public.ccsds.org/Pubs/660x1g2.pdf)
40
+ - [XTCE (Blue Book - Recommended Standard)](https://public.ccsds.org/Pubs/660x0b2.pdf)
41
+
42
+ ## Installation
43
+ ```bash
44
+ pip install space_packet_parser
45
+ ```
46
+
47
+ ## Missions using Space Packet Parser
48
+
49
+ [<img src="https://imap.princeton.edu/sites/g/files/toruqf1601/files/imap-mark-hor-multicolor-dark.png" alt="IMAP" height="70"/>](https://imap.princeton.edu/)
50
+ [<img src="https://clarreo-pathfinder.larc.nasa.gov/wp-content/uploads/sites/133/2019/08/clarreo_pathfinder_mission_patch_design_v4_decal_1_24_17.png" alt="CLARREO" height="140"/>](https://clarreo-pathfinder.larc.nasa.gov/)
51
+ [<img src="https://lasp.colorado.edu/libera/files/2021/02/Libera-Logo-HiRes.png" alt="Libera" height="70"/>](https://lasp.colorado.edu/libera/)
52
+ [<img src="https://lasp.colorado.edu/ctim/files/2023/01/CTIM_LOGO_350x100_centered_transparent.png" alt="CTIM-FD" height="70"/>](https://lasp.colorado.edu/ctim/)
53
+ [<img src="https://mms.gsfc.nasa.gov/images/promotional_materials/mms_decal_rgb_4in_trabk_72dpi.png" alt="MMS-FEEPS" height="140"/>](https://lasp.colorado.edu/mms/sdc/public/)
54
+
@@ -0,0 +1,30 @@
1
+ # Space Packet Parser
2
+
3
+ ----------
4
+ [![Test Status](https://github.com/medley56/space_packet_parser/actions/workflows/tests.yml/badge.svg)](https://github.com/medley56/space_packet_parser/actions/workflows/tests.yml)
5
+ [![Doc Status](https://readthedocs.org/projects/space-packet-parser/badge/?version=latest)](https://readthedocs.org/projects/space-packet-parser/)
6
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7735001.svg)](https://doi.org/10.5281/zenodo.7735001)
7
+
8
+ Documentation: [https://space-packet-parser.readthedocs.io/en/latest/](https://space-packet-parser.readthedocs.io/en/latest/)
9
+
10
+ Space Packet Parser is a package for decoding CCSDS telemetry packets according to an XTCE or CSV packet structure definition.
11
+ It is based on the UML model of the XTCE spec and aims to support all but the most esoteric elements of the
12
+ XTCE telemetry packet specification.
13
+
14
+ Resources:
15
+ - [XTCE (Green Book - Informational Report)](https://public.ccsds.org/Pubs/660x2g2.pdf)
16
+ - [XTCE Element Description (Green Book - Informational Report)](https://public.ccsds.org/Pubs/660x1g2.pdf)
17
+ - [XTCE (Blue Book - Recommended Standard)](https://public.ccsds.org/Pubs/660x0b2.pdf)
18
+
19
+ ## Installation
20
+ ```bash
21
+ pip install space_packet_parser
22
+ ```
23
+
24
+ ## Missions using Space Packet Parser
25
+
26
+ [<img src="https://imap.princeton.edu/sites/g/files/toruqf1601/files/imap-mark-hor-multicolor-dark.png" alt="IMAP" height="70"/>](https://imap.princeton.edu/)
27
+ [<img src="https://clarreo-pathfinder.larc.nasa.gov/wp-content/uploads/sites/133/2019/08/clarreo_pathfinder_mission_patch_design_v4_decal_1_24_17.png" alt="CLARREO" height="140"/>](https://clarreo-pathfinder.larc.nasa.gov/)
28
+ [<img src="https://lasp.colorado.edu/libera/files/2021/02/Libera-Logo-HiRes.png" alt="Libera" height="70"/>](https://lasp.colorado.edu/libera/)
29
+ [<img src="https://lasp.colorado.edu/ctim/files/2023/01/CTIM_LOGO_350x100_centered_transparent.png" alt="CTIM-FD" height="70"/>](https://lasp.colorado.edu/ctim/)
30
+ [<img src="https://mms.gsfc.nasa.gov/images/promotional_materials/mms_decal_rgb_4in_trabk_72dpi.png" alt="MMS-FEEPS" height="140"/>](https://lasp.colorado.edu/mms/sdc/public/)
@@ -1,19 +1,20 @@
1
1
  [tool.poetry]
2
2
  name = "space_packet_parser"
3
- version = "4.0.2"
3
+ version = "4.2.0"
4
4
  description = "A CCSDS telemetry packet decoding library based on the XTCE packet format description standard."
5
5
  license = "BSD-3-Clause"
6
6
  readme = "README.md"
7
7
  include = ["LICENSE.txt", "CITATION.cff"]
8
8
  authors = [
9
9
  "Gavin Medley <gavin.medley@lasp.colorado.edu>",
10
- "Michael Chambliss <michael.chambliss@lasp.colorado.edu>"
10
+ "Michael Chambliss <michael.chambliss@lasp.colorado.edu>",
11
+ "Greg Lucas <greg.lucas@lasp.colorado.edu>",
11
12
  ]
12
13
  maintainers = [
13
14
  "Gavin Medley <gavin.medley@lasp.colorado.edu>"
14
15
  ]
15
16
  repository = "https://github.com/medley56/space_packet_parser"
16
- homepage = ""
17
+ homepage = "https://space-packet-parser.readthedocs.io"
17
18
  keywords = [
18
19
  "ccsds",
19
20
  "xtce",
@@ -34,19 +35,20 @@ keywords = [
34
35
  ]
35
36
 
36
37
  [tool.poetry.dependencies]
37
- python = ">=3.8,<3.12"
38
- bitstring = ">=3.0.0,<5"
38
+ python = ">=3.8"
39
+ bitstring = ">=4.0.1"
39
40
 
40
41
  [tool.poetry.group.dev.dependencies]
41
- pylint = "^2"
42
- pytest = "^6"
43
- pytest-randomly = "^3"
44
- pytest-cov = "^3"
45
- pyyaml = "^6.0"
46
- sphinx = "^6.1.3"
47
- myst-parser = "^1.0.0"
48
- sphinx-autoapi = "^2.0.1"
49
- sphinx-rtd-theme = "^1.2.0"
42
+ pylint = "*"
43
+ pytest = "*"
44
+ pytest-randomly = "*"
45
+ pytest-cov = "*"
46
+ pyyaml = "*"
47
+ sphinx = "*"
48
+ myst-parser = "*"
49
+ sphinx-autoapi = "*"
50
+ sphinx-rtd-theme = "*"
51
+ coverage = "*"
50
52
 
51
53
  [tool.poetry.group.examples]
52
54
  optional = true
@@ -6,7 +6,7 @@ import io
6
6
  import logging
7
7
  import socket
8
8
  import time
9
- from typing import BinaryIO, Tuple
9
+ from typing import BinaryIO, Optional, Tuple
10
10
  import warnings
11
11
  # Installed
12
12
  import bitstring
@@ -15,19 +15,19 @@ from space_packet_parser import xtcedef, csvdef
15
15
 
16
16
  logger = logging.getLogger(__name__)
17
17
 
18
- CcsdsPacketHeaderElement = namedtuple('CcsdsPacketHeaderElement', ['name', 'format_string'])
18
+ CcsdsPacketHeaderElement = namedtuple('CcsdsPacketHeaderElement', ['name', 'nbits'])
19
19
 
20
20
  CCSDS_HEADER_DEFINITION = [
21
- CcsdsPacketHeaderElement('VERSION', 'uint:3'),
22
- CcsdsPacketHeaderElement('TYPE', 'uint:1'),
23
- CcsdsPacketHeaderElement('SEC_HDR_FLG', 'uint:1'),
24
- CcsdsPacketHeaderElement('PKT_APID', 'uint:11'),
25
- CcsdsPacketHeaderElement('SEQ_FLGS', 'uint:2'),
26
- CcsdsPacketHeaderElement('SRC_SEQ_CTR', 'uint:14'),
27
- CcsdsPacketHeaderElement('PKT_LEN', 'uint:16')
21
+ CcsdsPacketHeaderElement('VERSION', 3),
22
+ CcsdsPacketHeaderElement('TYPE', 1),
23
+ CcsdsPacketHeaderElement('SEC_HDR_FLG', 1),
24
+ CcsdsPacketHeaderElement('PKT_APID', 11),
25
+ CcsdsPacketHeaderElement('SEQ_FLGS', 2),
26
+ CcsdsPacketHeaderElement('SRC_SEQ_CTR', 14),
27
+ CcsdsPacketHeaderElement('PKT_LEN', 16)
28
28
  ]
29
29
 
30
- CCSDS_HEADER_LENGTH_BITS = 48
30
+ CCSDS_HEADER_LENGTH_BYTES = 6
31
31
 
32
32
  Packet = namedtuple('Packet', ['header', 'data'])
33
33
 
@@ -35,7 +35,8 @@ Packet = namedtuple('Packet', ['header', 'data'])
35
35
  class ParsedDataItem(xtcedef.AttrComparable):
36
36
  """Representation of a parsed parameter"""
37
37
 
38
- def __init__(self, name: str, raw_value: any, unit: str = None, derived_value: float or str = None):
38
+ def __init__(self, name: str, raw_value: any, unit: str = None, derived_value: float or str = None,
39
+ short_description: str = None, long_description: str = None):
39
40
  """Constructor
40
41
 
41
42
  Parameters
@@ -48,6 +49,10 @@ class ParsedDataItem(xtcedef.AttrComparable):
48
49
  Raw representation of the parsed value. May be lots of different types but most often an integer
49
50
  derived_value : float or str
50
51
  May be a calibrated value or an enum lookup
52
+ short_description : str
53
+ Parameter short description
54
+ long_description : str
55
+ Parameter long description
51
56
  """
52
57
  if name is None or raw_value is None:
53
58
  raise ValueError("Invalid ParsedDataItem. Must define name and raw_value.")
@@ -55,6 +60,8 @@ class ParsedDataItem(xtcedef.AttrComparable):
55
60
  self.raw_value = raw_value
56
61
  self.unit = unit
57
62
  self.derived_value = derived_value
63
+ self.short_description = short_description
64
+ self.long_description = long_description
58
65
 
59
66
  def __repr__(self):
60
67
  return (f"{self.__class__.__name__}("
@@ -97,62 +104,30 @@ class PacketParser:
97
104
  self.word_size = word_size
98
105
 
99
106
  @staticmethod
100
- def _parse_header(packet_data: bitstring.ConstBitStream,
101
- start_position: int = None,
102
- reset_cursor: bool = False) -> dict:
107
+ def _parse_header(packet_data: bytes) -> dict:
103
108
  """Parses the CCSDS standard header.
104
109
 
105
110
  Parameters
106
111
  ----------
107
- packet_data : bitstring.ConstBitStream
108
- Binary data stream of packet data.
109
- start_position : int
110
- Position from which to start parsing. If not provided, will start whenever the cursor currently is.
111
- reset_cursor : bool
112
- If True, upon parsing the header data, reset the cursor to the original position in the stream.
113
- This still applies even if start_position is specified. start_position will be used only for parsing the
114
- header and then the cursor will be returned to the location it was at before this function was called.
112
+ packet_data : bytes
113
+ 6 bytes of binary data.
115
114
 
116
115
  Returns
117
116
  -------
118
117
  header : dict
119
118
  Dictionary of header items.
120
119
  """
121
- original_cursor_position = packet_data.pos
122
-
123
- if start_position:
124
- packet_data.pos = start_position
125
-
126
- header = {
127
- item.name: ParsedDataItem(name=item.name, unit=None, raw_value=packet_data.read(item.format_string))
128
- for item in CCSDS_HEADER_DEFINITION
129
- }
130
-
131
- if reset_cursor:
132
- packet_data.pos = original_cursor_position
133
-
120
+ header = {}
121
+ current_bit = 0
122
+ for item in CCSDS_HEADER_DEFINITION:
123
+ header[item.name] = ParsedDataItem(name=item.name,
124
+ unit=None,
125
+ raw_value=_extract_bits(packet_data, current_bit, item.nbits))
126
+ current_bit += item.nbits
134
127
  return header
135
128
 
136
- @staticmethod
137
- def _total_packet_bits_from_pkt_len(pkt_len: int):
138
- """Calculate the total length of a CCSDS packet in bits based on the PKT_LEN field in its header.
139
-
140
- Parameters
141
- ----------
142
- pkt_len : int
143
- PKT_LEN value from CCSDS header
144
-
145
- Returns
146
- -------
147
- : int
148
- Length, in bits of the packet
149
-
150
- """
151
- # 4.1.3.5.3 The length count C shall be expressed as:
152
- # C = (Total Number of Octets in the Packet Data Field) – 1
153
- # We also just reparsed the CCSDS header though as well, so that's an additional 6 octets
154
- return 8 * (pkt_len + 1 + 6)
155
-
129
+ # DEPRECATED! Remove in next major release along with CSV parser
130
+ # pylint: disable=inconsistent-return-statements
156
131
  def _determine_packet_by_restrictions(self, parsed_header: dict) -> Tuple[str, list]:
157
132
  """Examines a dictionary representation of a CCSDS header and determines which packet type applies.
158
133
  This packet type must be unique. If the header data satisfies the restrictions for more than one packet
@@ -206,6 +181,7 @@ class PacketParser:
206
181
  "Unable to choose a packet type to parse. "
207
182
  "Note: Restricting container inheritance based on non-header data items is not possible in a "
208
183
  "general way and is not supported by this package.", partial_data=parsed_header)
184
+ # pylint: enable=inconsistent-return-statements
209
185
 
210
186
  @staticmethod
211
187
  def parse_packet(packet_data: bitstring.ConstBitStream,
@@ -237,7 +213,9 @@ class PacketParser:
237
213
  name=p.name,
238
214
  unit=p.parameter_type.unit,
239
215
  raw_value=parsed_value,
240
- derived_value=derived_value
216
+ derived_value=derived_value,
217
+ short_description=p.short_description,
218
+ long_description=p.long_description
241
219
  )
242
220
 
243
221
  def _parse_sequence_container(sc: xtcedef.SequenceContainer):
@@ -250,11 +228,7 @@ class PacketParser:
250
228
  parsed_items = {}
251
229
  current_container: xtcedef.SequenceContainer = containers[root_container_name]
252
230
  while True:
253
- for entry in current_container.entry_list:
254
- if isinstance(entry, xtcedef.Parameter):
255
- _parse_parameter(entry)
256
- elif isinstance(entry, xtcedef.SequenceContainer):
257
- _parse_sequence_container(entry)
231
+ _parse_sequence_container(current_container)
258
232
 
259
233
  valid_inheritors = []
260
234
  for inheritor_name in current_container.inheritors:
@@ -320,23 +294,25 @@ class PacketParser:
320
294
  name=parameter.name,
321
295
  unit=parameter.parameter_type.unit,
322
296
  raw_value=parsed_value,
323
- derived_value=derived_value
297
+ derived_value=derived_value,
298
+ short_description=parameter.short_description,
299
+ long_description=parameter.long_description
324
300
  )
325
301
 
326
302
  return Packet(header=header, data=user_data)
327
303
 
328
304
  @staticmethod
329
- def print_progress(current_bits: int, total_bits: int or None,
305
+ def print_progress(current_bytes: int, total_bytes: int or None,
330
306
  start_time_ns: int, current_packets: int,
331
307
  end: str = '\r', log: bool = False):
332
308
  """Prints a progress bar, including statistics on parsing rate.
333
309
 
334
310
  Parameters
335
311
  ----------
336
- current_bits : int
337
- Number of bits parsed so far.
338
- total_bits : int
339
- Number of total bits to parse (if known)
312
+ current_bytes : int
313
+ Number of bytes parsed so far.
314
+ total_bytes : int
315
+ Number of total bytes to parse (if known)
340
316
  current_packets : int
341
317
  Number of packets parsed so far.
342
318
  start_time_ns : int
@@ -349,26 +325,26 @@ class PacketParser:
349
325
  progress_char = "="
350
326
  bar_length = 20
351
327
 
352
- if total_bits is not None: # If we actually have an endpoint (i.e. not using a socket)
353
- percentage = int((current_bits / total_bits) * 100) # Percent Completed Calculation
354
- progress = int((bar_length * current_bits) / total_bits) # Progress Done Calculation
328
+ if total_bytes is not None: # If we actually have an endpoint (i.e. not using a socket)
329
+ percentage = int((current_bytes / total_bytes) * 100) # Percent Completed Calculation
330
+ progress = int((bar_length * current_bytes) / total_bytes) # Progress Done Calculation
355
331
  else:
356
332
  percentage = "???"
357
333
  progress = 0
358
334
 
359
335
  elapsed_ns = time.time_ns() - start_time_ns
360
336
  delta = dt.timedelta(microseconds=elapsed_ns / 1E3)
361
- kbps = int(current_bits * 1E6 / elapsed_ns)
337
+ kbps = int(current_bytes // 8 * 1E6 / elapsed_ns)
362
338
  pps = int(current_packets * 1E9 / elapsed_ns)
363
339
  info_str = f"[Elapsed: {delta}, " \
364
- f"Parsed {current_bits} bits ({current_packets} packets) " \
340
+ f"Parsed {current_bytes} bytes ({current_packets} packets) " \
365
341
  f"at {kbps}kb/s ({pps}pkts/s)]"
366
342
  loadbar = f"Progress: [{progress*progress_char:{bar_length}}]{percentage}% {info_str}"
367
343
  print(loadbar, end=end)
368
344
  if log is True:
369
345
  logger.info(loadbar)
370
346
 
371
- def generator(self,
347
+ def generator(self, # pylint: disable=too-many-branches,too-many-statements
372
348
  binary_data: bitstring.ConstBitStream or BinaryIO or socket.socket,
373
349
  parse_bad_pkts: bool = True,
374
350
  skip_header_bits: int = 0,
@@ -376,7 +352,7 @@ class PacketParser:
376
352
  ccsds_headers_only: bool = False,
377
353
  yield_unrecognized_packet_errors: bool = False,
378
354
  show_progress: bool = False,
379
- buffer_read_size_bytes: int = 4096):
355
+ buffer_read_size_bytes: Optional[int] = None):
380
356
  """Create and return a Packet generator that reads from a ConstBitStream or a filelike object or a socket.
381
357
 
382
358
  Creating a generator object to return allows the user to create
@@ -401,6 +377,7 @@ class PacketParser:
401
377
  ccsds_headers_only : bool, Optional
402
378
  If True, only parses the packet headers (does not use the provided packet definition).
403
379
  yield_unrecognized_packet_errors : bool, Optional
380
+ Default False.
404
381
  If False, UnrecognizedPacketTypeErrors are caught silently and parsing continues to the next packet.
405
382
  If True, the generator will yield an UnrecognizedPacketTypeError in the event of an unrecognized
406
383
  packet. Note: These exceptions are not raised by default but are instead returned so that the generator
@@ -411,28 +388,24 @@ class PacketParser:
411
388
  ends.
412
389
  buffer_read_size_bytes : int, Optional
413
390
  Number of bytes to read from e.g. a BufferedReader or socket binary data source on each read attempt.
414
- Default is 4096 bytes.
391
+ Default is 4096 bytes from a socket, -1 (full read) from a file.
415
392
 
416
393
  Yields
417
394
  -------
418
- : Packet or UnrecognizedPacketTypeError
395
+ Packet or UnrecognizedPacketTypeError
419
396
  Generator yields Packet objects containing the parsed packet data for each subsequent packet.
420
397
  If yield_unrecognized_packet_errors is True, it will yield an unraised exception object,
421
398
  which can be raised or used for debugging purposes.
422
399
  """
423
400
 
424
- def fill_read_buffer(source: bitstring.ConstBitStream or BinaryIO or socket.socket,
425
- buffer: bitstring.BitStream,
426
- read_size_bytes: int) -> int:
427
- """Read data from a source and add it to an existing buffer (BitStream).
401
+ def read_bytes_from_source(source: bitstring.ConstBitStream or BinaryIO or socket.socket,
402
+ read_size_bytes: int) -> int:
403
+ """Read data from a source and return the bytes read.
428
404
 
429
405
  Parameters
430
406
  ----------
431
407
  source : bitstring.ConstBitStream or BinaryIO or socket.socket
432
408
  Source of data.
433
- buffer : bitstring.BitStream
434
- A reference to a rotating buffer to which the new data is appended. Mutating this changes the data
435
- available to the caller by reference so we don't return it.
436
409
  read_size_bytes : int
437
410
  Max number of bytes to read from the source per read attempt. For sockets, this should be a small
438
411
  power of 2 (e.g. 4096) due to networking and hardware conventions. For a file or ConstBitStream object
@@ -441,99 +414,120 @@ class PacketParser:
441
414
 
442
415
  Returns
443
416
  -------
444
- result : int
445
- Number of bits added to the buffer. Note that the buffer may still have nonzero length from previous
446
- data even when this returns zero.
417
+ bytes
418
+ The bytes that were read from the source.
447
419
  """
448
420
  if isinstance(source, io.BufferedIOBase):
449
- new_bytes = source.read(read_size_bytes)
450
- buffer += new_bytes
451
- return len(new_bytes)*8
452
-
421
+ return source.read(read_size_bytes)
453
422
  if isinstance(source, socket.socket):
454
- new_bytes = source.recv(read_size_bytes)
455
- buffer += new_bytes # Append BitStream with newly read bytes
456
- return len(new_bytes)*8
457
-
423
+ return source.recv(read_size_bytes)
458
424
  if isinstance(source, bitstring.ConstBitStream):
425
+ # This either reads read_size_bytes bytes or it just reads to the end of the data
459
426
  new_bits = source[source.pos:source.pos + read_size_bytes * 8]
460
- assert isinstance(new_bits, bitstring.ConstBitStream)
461
- source.pos += len(new_bits) # This way we never overshoot the end of the data
462
- buffer += new_bits
463
- return len(new_bits)
464
-
465
- raise ValueError(f"Unrecognized data source: {source}")
427
+ source.pos += len(new_bits) # Set the source.pos to exactly where we read to
428
+ return new_bits.tobytes()
429
+ if isinstance(source, io.TextIOWrapper):
430
+ raise IOError("Packet data file opened in TextIO mode. You must open packet data in binary mode.")
431
+ raise IOError(f"Unrecognized data source: {source}")
466
432
 
467
433
  # ========
468
434
  # Start of generator
469
435
  # ========
470
436
  if isinstance(binary_data, bitstring.ConstBitStream):
471
- total_length_bits = len(binary_data)
437
+ total_length_bytes = len(binary_data) // 8
438
+ if buffer_read_size_bytes is None:
439
+ # Default to a full read of the bitstream
440
+ buffer_read_size_bytes = total_length_bytes
472
441
  logger.info(
473
- f"Creating packet generator from pre-loaded ConstBitStream. Total length is {total_length_bits}")
442
+ f"Creating packet generator from pre-loaded ConstBitStream. Total length is {total_length_bytes} bytes")
474
443
  elif isinstance(binary_data, io.BufferedIOBase):
475
- total_length_bits = 8 * binary_data.seek(0, io.SEEK_END) # This is probably preferable to len
444
+ if buffer_read_size_bytes is None:
445
+ # Default to a full read of the file
446
+ buffer_read_size_bytes = -1
447
+ total_length_bytes = binary_data.seek(0, io.SEEK_END) # This is probably preferable to len
476
448
  binary_data.seek(0, 0)
477
449
  logger.info(f"Creating packet generator from a filelike object, {binary_data}. "
478
- f"Total length is {total_length_bits}bits")
450
+ f"Total length is {total_length_bytes} bytes")
479
451
  else: # It's a socket and we don't know how much data we will get
480
452
  logger.info("Creating packet generator to read from a socket. Total length to parse is unknown.")
481
- total_length_bits = None # We don't know how long it is
453
+ total_length_bytes = None # We don't know how long it is
454
+ if buffer_read_size_bytes is None:
455
+ # Default to 4096 bytes from a socket
456
+ buffer_read_size_bytes = 4096
482
457
 
483
458
  # ========
484
459
  # Packet loop. Each iteration of this loop yields a ParsedPacket object
485
460
  # ========
486
461
  start_time = time.time_ns()
487
- n_bits_parsed = 0 # Keep track of how many bits we have parsed
462
+ n_bytes_parsed = 0 # Keep track of how many bytes we have parsed
488
463
  n_packets_parsed = 0 # Keep track of how many packets we have parsed
489
- read_buffer = bitstring.BitStream() # Not const because it's a rotating buffer
464
+ read_buffer = b"" # Empty bytes object to start
465
+ skip_header_bytes = skip_header_bits // 8 # Internally keep track of bytes
466
+ current_pos = 0 # Keep track of where we are in the buffer
490
467
  while True:
491
- if total_length_bits and n_bits_parsed == total_length_bits:
468
+ if total_length_bytes and n_bytes_parsed == total_length_bytes:
492
469
  break # Exit if we know the length and we've reached it
493
470
 
494
471
  if show_progress is True:
495
- self.print_progress(current_bits=n_bits_parsed, total_bits=total_length_bits,
472
+ self.print_progress(current_bytes=n_bytes_parsed, total_bytes=total_length_bytes,
496
473
  start_time_ns=start_time, current_packets=n_packets_parsed)
497
474
 
475
+ if current_pos > 20_000_000:
476
+ # Only trim the buffer after 20 MB read to prevent modifying
477
+ # the bitstream and trimming after every packet
478
+ read_buffer = read_buffer[current_pos:]
479
+ current_pos = 0
480
+
498
481
  # Fill buffer enough to parse a header
499
- while len(read_buffer) < skip_header_bits + CCSDS_HEADER_LENGTH_BITS:
500
- result = fill_read_buffer(binary_data, read_buffer,
501
- read_size_bytes=buffer_read_size_bytes)
482
+ while len(read_buffer) - current_pos < skip_header_bytes + CCSDS_HEADER_LENGTH_BYTES:
483
+ result = read_bytes_from_source(binary_data, read_size_bytes=buffer_read_size_bytes)
502
484
  if not result: # If there is verifiably no more data to add, break
503
485
  break
504
-
505
- read_buffer.pos += skip_header_bits
506
- header = self._parse_header(read_buffer, reset_cursor=True)
507
- specified_total_packet_length_bits = self._total_packet_bits_from_pkt_len(header['PKT_LEN'].raw_value)
508
- n_packets_parsed += 1 # Consider it a counted packet once we've parsed the header
486
+ read_buffer += result
487
+ # Skip the header bytes
488
+ current_pos += skip_header_bytes
489
+ header_bytes = read_buffer[current_pos:current_pos + CCSDS_HEADER_LENGTH_BYTES]
490
+ header = self._parse_header(header_bytes)
491
+
492
+ # per the CCSDS spec
493
+ # 4.1.3.5.3 The length count C shall be expressed as:
494
+ # C = (Total Number of Octets in the Packet Data Field) – 1
495
+ n_bytes_data = header['PKT_LEN'].raw_value + 1
496
+ n_bytes_packet = CCSDS_HEADER_LENGTH_BYTES + n_bytes_data
497
+
498
+ # Consider it a counted packet once we've parsed the header
499
+ # and update the number of bits parsed
500
+ n_packets_parsed += 1
501
+ n_bytes_parsed += skip_header_bytes + n_bytes_packet
509
502
  if ccsds_headers_only is True:
510
- # Trim read buffer (this also reduces memory usage over time for reading a ConstBitStream)
511
- n_bits_parsed += skip_header_bits + specified_total_packet_length_bits
512
- read_buffer = read_buffer[specified_total_packet_length_bits + skip_header_bits:]
503
+ # update the current position to the end of the packet data
504
+ current_pos += n_bytes_packet
513
505
  yield Packet(header=header, data=None)
514
506
  continue
515
507
 
516
508
  # Based on PKT_LEN fill buffer enough to read a full packet
517
- while len(read_buffer) < skip_header_bits + specified_total_packet_length_bits:
518
- result = fill_read_buffer(binary_data, read_buffer,
519
- read_size_bytes=buffer_read_size_bytes)
509
+ while len(read_buffer) - current_pos < n_bytes_packet:
510
+ result = read_bytes_from_source(binary_data, read_size_bytes=buffer_read_size_bytes)
520
511
  if not result: # If there is verifiably no more data to add, break
521
512
  break
522
-
513
+ read_buffer += result
514
+
515
+ # current_pos is still before the header, so we are reading the entire packet here
516
+ packet_bytes = read_buffer[current_pos:current_pos + n_bytes_packet]
517
+ current_pos += n_bytes_packet
518
+ # Send bitstring data to the parser for now
519
+ # TODO: Look into parsing the raw bytes directly
520
+ bitstring_packet = bitstring.ConstBitStream(packet_bytes)
523
521
  try:
524
522
  if isinstance(self.packet_definition, xtcedef.XtcePacketDefinition):
525
- packet = self.parse_packet(read_buffer,
523
+ packet = self.parse_packet(bitstring_packet,
526
524
  self.packet_definition.named_containers,
527
525
  root_container_name=root_container_name,
528
526
  word_size=self.word_size)
529
527
  else:
530
528
  _, parameter_list = self._determine_packet_by_restrictions(header)
531
- packet = self.legacy_parse_packet(read_buffer, parameter_list, word_size=self.word_size)
529
+ packet = self.legacy_parse_packet(bitstring_packet, parameter_list, word_size=self.word_size)
532
530
  except UnrecognizedPacketTypeError as e:
533
- # Regardless of whether we handle the error, we still want to chop the read_buffer in preparation
534
- # for parsing the next packet
535
- n_bits_parsed += skip_header_bits + specified_total_packet_length_bits
536
- read_buffer = read_buffer[skip_header_bits + specified_total_packet_length_bits:]
537
531
  logger.debug(f"Unrecognized error on packet with APID {header['PKT_APID'].raw_value}'")
538
532
  if yield_unrecognized_packet_errors is True:
539
533
  # Yield the caught exception without raising it (raising ends generator)
@@ -547,22 +541,46 @@ class PacketParser:
547
541
  f"{packet.header['PKT_LEN'].raw_value}. This might be because the CCSDS header is "
548
542
  f"incorrectly represented in your packet definition document.")
549
543
 
550
- actual_length_parsed = read_buffer.pos - skip_header_bits
551
-
552
- if actual_length_parsed != specified_total_packet_length_bits:
544
+ actual_length_parsed = bitstring_packet.pos // 8
545
+ if actual_length_parsed != n_bytes_packet:
553
546
  logger.warning(f"Parsed packet length "
554
- f"({actual_length_parsed}b) did not match "
555
- f"length specified in header ({specified_total_packet_length_bits}b). "
556
- f"Updating bit string position to correct position "
547
+ f"({actual_length_parsed}B) did not match "
548
+ f"length specified in header ({n_bytes_packet}B). "
549
+ f"Updating the position to the correct position "
557
550
  "indicated by CCSDS header.")
558
551
  if not parse_bad_pkts:
559
552
  logger.warning("Skipping (not yielding) bad packet because parse_bad_pkts is falsy.")
560
553
  continue
561
- n_bits_parsed += skip_header_bits + specified_total_packet_length_bits
562
- read_buffer = read_buffer[specified_total_packet_length_bits + skip_header_bits:]
554
+
563
555
  yield packet
564
556
 
565
557
  if show_progress is True:
566
- self.print_progress(current_bits=n_bits_parsed, total_bits=total_length_bits,
558
+ self.print_progress(current_bytes=n_bytes_parsed, total_bytes=total_length_bytes,
567
559
  start_time_ns=start_time, current_packets=n_packets_parsed,
568
560
  end="\n", log=True)
561
+
562
+
563
+ def _extract_bits(data: bytes, start_bit: int, nbits: int):
564
+ """Extract nbits from the data starting from the least significant end.
565
+
566
+ If data = b"abcdefgh", start_bit = 2, nbits = 3, then the bits extracted are
567
+ "cde" and those are turned into a Python integer and returned.
568
+
569
+ Parameters
570
+ ----------
571
+ data : bytes
572
+ Data to extract bits from
573
+ start_bit : int
574
+ Starting bit location
575
+ nbits : int
576
+ Number of bits to extract
577
+
578
+ Returns
579
+ -------
580
+ int
581
+ Extracted bits as an integer
582
+ """
583
+ # Shift the value to the right to get the start bit to the least significant position
584
+ # Then mask out the bits we want to keep
585
+ value = int.from_bytes(data, byteorder="big")
586
+ return (value >> (len(data) * 8 - start_bit - nbits)) & (2 ** nbits - 1)
@@ -237,7 +237,7 @@ class Comparison(MatchCriteria):
237
237
  if isinstance(required_value, str):
238
238
  parsed_value = f"'{parsed_value}'"
239
239
  required_value = f"'{required_value}'"
240
- return eval(f"{parsed_value} {operator} {required_value}")
240
+ return eval(f"{parsed_value} {operator} {required_value}") # pylint: disable=eval-used
241
241
 
242
242
 
243
243
  class Condition(MatchCriteria):
@@ -392,7 +392,7 @@ class Condition(MatchCriteria):
392
392
  if isinstance(left_value, str):
393
393
  left_value = f"'{left_value}'"
394
394
  right_value = f"'{right_value}'"
395
- return eval(f"{left_value} {operator} {right_value}")
395
+ return eval(f"{left_value} {operator} {right_value}") # pylint: disable=eval-used
396
396
 
397
397
 
398
398
  Anded = namedtuple('Anded', ['conditions', 'ors'])
@@ -463,13 +463,15 @@ class BooleanExpression(MatchCriteria):
463
463
  return cls(expression=_parse_ored(element.find('xtce:ORedConditions', ns)))
464
464
  raise ValueError(f"Failed to parse {element}")
465
465
 
466
- def evaluate(self, parsed_data: dict, **kwargs) -> bool:
466
+ def evaluate(self, parsed_data: dict, current_parsed_value: int or float = None) -> bool:
467
467
  """Evaluate the criteria in the BooleanExpression down to a single boolean.
468
468
 
469
469
  Parameters
470
470
  ----------
471
471
  parsed_data : dict
472
472
  Dictionary of parsed parameter data so far. Used to evaluate truthyness of the match criteria.
473
+ current_parsed_value : int or float, Optional
474
+ Current value being parsed.
473
475
 
474
476
  Returns
475
477
  -------
@@ -862,10 +864,12 @@ class ContextCalibrator(AttrComparable):
862
864
  if context_match_element.find('xtce:Comparison', ns) is not None:
863
865
  return [Comparison.from_match_criteria_xml_element(
864
866
  context_match_element.find('xtce:Comparison', ns), ns)]
865
- # TODO: Implement handling of BooleanExpression
866
- raise NotImplementedError("ContextCalibrator doesn't contain Comparison on ComparisonList. This probably"
867
- "means the match criteria is an unsupported type (BooleanExpression or "
868
- "CustomAlgorithm).")
867
+ if context_match_element.find('xtce:BooleanExpression', ns) is not None:
868
+ return [BooleanExpression.from_match_criteria_xml_element(
869
+ context_match_element.find('xtce:BooleanExpression', ns), ns)]
870
+ raise NotImplementedError("ContextCalibrator doesn't contain Comparison, ComparisonList, or BooleanExpression. "
871
+ "This probably means the match criteria is an unsupported type "
872
+ "(CustomAlgorithm).")
869
873
 
870
874
  @classmethod
871
875
  def from_context_calibrator_xml_element(cls, element: ElementTree.Element, ns: dict):
@@ -1140,6 +1144,7 @@ class StringDataEncoding(DataEncoding):
1140
1144
  : int or None
1141
1145
  Number of bits to skip after parsing the string
1142
1146
  """
1147
+ # pylint: disable=too-many-branches
1143
1148
  skip_bits_after = 0 # Gets modified if we have a termination character
1144
1149
  if self.fixed_length:
1145
1150
  strlen_bits = self.fixed_length
@@ -1161,10 +1166,7 @@ class StringDataEncoding(DataEncoding):
1161
1166
  strlen_bits = parsed_data[self.dynamic_length_reference].raw_value
1162
1167
  strlen_bits = int(strlen_bits)
1163
1168
  elif self.termination_character is not None:
1164
- print(f"hex termination character: {self.termination_character}")
1165
1169
  termination_char_utf8_bytes = bytes.fromhex(self.termination_character)
1166
- print(f"bytes termination character (utf-8): {termination_char_utf8_bytes}, "
1167
- f"len={len(termination_char_utf8_bytes)}")
1168
1170
 
1169
1171
  if self.encoding in ['utf-16-le', 'utf-16-be']:
1170
1172
  bytes_per_char = 2
@@ -1178,12 +1180,9 @@ class StringDataEncoding(DataEncoding):
1178
1180
  bits_per_byte = 8
1179
1181
  look_ahead_n_bytes = 0
1180
1182
  while look_ahead_n_bytes <= len(packet_data) - packet_data.pos:
1181
- print(f"looking ahead {look_ahead_n_bytes} bytes")
1182
1183
  look_ahead = packet_data.peek(f'bytes:{look_ahead_n_bytes}') # Outputs UTF-8 encoded byte string
1183
1184
  look_ahead = look_ahead.decode('utf-8').encode(self.encoding) # Force specified encoding
1184
- print(f"string so far: {look_ahead}")
1185
1185
  if termination_char_utf8_bytes in look_ahead:
1186
- print('Found termination character.')
1187
1186
  # Implicit assumption of one termination character in specified encoding
1188
1187
  tclen_bits = bytes_per_char * bits_per_byte
1189
1188
  strlen_bits = (look_ahead_n_bytes * bits_per_byte) - tclen_bits
@@ -1199,6 +1198,7 @@ class StringDataEncoding(DataEncoding):
1199
1198
  if self.length_linear_adjuster is not None:
1200
1199
  strlen_bits = self.length_linear_adjuster(strlen_bits)
1201
1200
  return f"bytes:{strlen_bits // 8}", skip_bits_after
1201
+ # pylint: enable=too-many-branches
1202
1202
 
1203
1203
  def parse_value(self, packet_data: bitstring.ConstBitStream, parsed_data: dict, **kwargs):
1204
1204
  """Parse a value from packet data, possibly using previously parsed data items to inform parsing.
@@ -1228,7 +1228,8 @@ class StringDataEncoding(DataEncoding):
1228
1228
  Strings in XTCE can be described in three ways:
1229
1229
 
1230
1230
  1. Using a termination character that marks the end of the string.
1231
- 2. Using a fixed length, which may be derived from referenced parameter either directly or via a discrete lookup table.
1231
+ 2. Using a fixed length, which may be derived from referenced parameter either directly or via a discrete
1232
+ lookup table.
1232
1233
  3. Using a leading size field that describes the size of the following string.
1233
1234
 
1234
1235
  Parameters
@@ -1394,7 +1395,10 @@ class IntegerDataEncoding(NumericDataEncoding):
1394
1395
  : cls
1395
1396
  """
1396
1397
  size_in_bits = int(element.attrib['sizeInBits'])
1397
- encoding = element.attrib['encoding']
1398
+ if 'encoding' in element.attrib:
1399
+ encoding = element.attrib['encoding']
1400
+ else:
1401
+ encoding = "unsigned"
1398
1402
  calibrator = cls.get_default_calibrator(element, ns)
1399
1403
  context_calibrators = cls.get_context_calibrators(element, ns)
1400
1404
  return cls(size_in_bits=size_in_bits, encoding=encoding,
@@ -1431,6 +1435,9 @@ class FloatDataEncoding(NumericDataEncoding):
1431
1435
  f"Must be one of {self._supported_encodings}.")
1432
1436
  if encoding == 'MIL-1750A':
1433
1437
  raise NotImplementedError("MIL-1750A encoded floats are not supported by this library yet.")
1438
+ if encoding == 'IEEE-754' and size_in_bits not in (16, 32, 64):
1439
+ raise ValueError(f"Invalid size_in_bits value for IEEE-754 FloatDataEncoding, {size_in_bits}. "
1440
+ "Must be 16, 32, or 64.")
1434
1441
  super().__init__(size_in_bits, encoding=encoding,
1435
1442
  default_calibrator=default_calibrator, context_calibrators=context_calibrators)
1436
1443
 
@@ -1631,7 +1638,7 @@ class ParameterType(AttrComparable, metaclass=ABCMeta):
1631
1638
 
1632
1639
  @classmethod
1633
1640
  def from_parameter_type_xml_element(cls, element: ElementTree.Element, ns: dict):
1634
- """Create an IntegerParameterType from an <xtce:IntegerParameterType> XML element.
1641
+ """Create a *ParameterType from an <xtce:*ParameterType> XML element.
1635
1642
 
1636
1643
  Parameters
1637
1644
  ----------
@@ -1642,7 +1649,7 @@ class ParameterType(AttrComparable, metaclass=ABCMeta):
1642
1649
 
1643
1650
  Returns
1644
1651
  -------
1645
- : IntegerParameterType
1652
+ : ParameterType
1646
1653
  """
1647
1654
  name = element.attrib['name']
1648
1655
  unit = cls.get_units(element, ns)
@@ -1666,6 +1673,7 @@ class ParameterType(AttrComparable, metaclass=ABCMeta):
1666
1673
  -------
1667
1674
  : str or None
1668
1675
  """
1676
+ # Assume we are not parsing a Time Parameter Type, which stores units differently
1669
1677
  units = parameter_type_element.findall('xtce:UnitSet/xtce:Unit', ns)
1670
1678
  # TODO: Implement multiple unit elements for compound unit definitions
1671
1679
  assert len(units) <= 1, f"Found {len(units)} <xtce:Unit> elements in a single <xtce:UnitSet>." \
@@ -1673,6 +1681,7 @@ class ParameterType(AttrComparable, metaclass=ABCMeta):
1673
1681
  f"and is not yet supported by this library."
1674
1682
  if units:
1675
1683
  return " ".join([u.text for u in units])
1684
+ # Units are optional so return None if they aren't specified
1676
1685
  return None
1677
1686
 
1678
1687
  @staticmethod
@@ -1693,9 +1702,10 @@ class ParameterType(AttrComparable, metaclass=ABCMeta):
1693
1702
  """
1694
1703
  for data_encoding in [StringDataEncoding, IntegerDataEncoding, FloatDataEncoding, BinaryDataEncoding]:
1695
1704
  # Try to find each type of data encoding element. If we find one, we assume it's the only one.
1696
- element = parameter_type_element.find(f"xtce:{data_encoding.__name__}", ns)
1705
+ element = parameter_type_element.find(f".//xtce:{data_encoding.__name__}", ns)
1697
1706
  if element is not None:
1698
1707
  return data_encoding.from_data_encoding_xml_element(element, ns)
1708
+ return None
1699
1709
 
1700
1710
  def parse_value(self, packet_data: bitstring.ConstBitStream, parsed_data: dict, **kwargs):
1701
1711
  """Using the parameter type definition and associated data encoding, parse a value from a bit stream starting
@@ -1868,10 +1878,222 @@ class BinaryParameterType(ParameterType):
1868
1878
  self.encoding = encoding
1869
1879
 
1870
1880
 
1871
- class Parameter:
1881
+ class BooleanParameterType(ParameterType):
1882
+ """<xtce:BooleanParameterType>"""
1883
+
1884
+ def __init__(self, name: str, encoding: DataEncoding, unit: str = None):
1885
+ """Constructor that just issues a warning if the encoding is String or Binary"""
1886
+ if isinstance(encoding, (BinaryDataEncoding, StringDataEncoding)):
1887
+ warnings.warn(f"You are encoding a BooleanParameterType with a {type(encoding)} encoding."
1888
+ f"This is almost certainly a very bad idea because the behavior of string and binary "
1889
+ f"encoded booleans is not specified in XTCE. e.g. is the string \"0\" truthy?")
1890
+ super().__init__(name, encoding, unit)
1891
+
1892
+ def parse_value(self, packet_data: bitstring.ConstBitStream, parsed_data: dict, **kwargs):
1893
+ """Using the parameter type definition and associated data encoding, parse a value from a bit stream starting
1894
+ at the current cursor position.
1895
+
1896
+ Parameters
1897
+ ----------
1898
+ packet_data : bitstring.ConstBitStream
1899
+ Binary packet data with cursor at the beginning of this parameter's data field.
1900
+ parsed_data : dict
1901
+ Previously parsed data
1902
+
1903
+ Returns
1904
+ -------
1905
+ parsed_value : int
1906
+ Raw encoded value
1907
+ derived_value : str
1908
+ Resulting boolean representation of the encoded raw value
1909
+ """
1910
+ raw, _ = super().parse_value(packet_data, parsed_data, **kwargs)
1911
+ # Note: This behaves very strangely for String and Binary data encodings.
1912
+ # Don't use those for Boolean parameters. The behavior isn't specified well in XTCE.
1913
+ return raw, bool(raw)
1914
+
1915
+
1916
+ class TimeParameterType(ParameterType, metaclass=ABCMeta):
1917
+ """Abstract class for time parameter types"""
1918
+
1919
+ def __init__(self, name: str, encoding: DataEncoding, unit: str = None,
1920
+ epoch: str = None, offset_from: str = None):
1921
+ """Constructor
1922
+
1923
+ Parameters
1924
+ ----------
1925
+ name : str
1926
+ Parameter type name. Usually something like 'MSN__PARAM_Type'.
1927
+ encoding : DataEncoding
1928
+ How the data is encoded. e.g. IntegerDataEncoding, StringDataEncoding, etc.
1929
+ unit : str, Optional
1930
+ String describing the unit for the stored value. Note that if a scale and offset are provided on
1931
+ the Encoding element, the unit applies to the scaled value, not the raw value.
1932
+ epoch : str, Optional
1933
+ String describing the starting epoch for the date or datetime encoded in the parameter.
1934
+ Must be xs:date, xs:dateTime, or one of the following: "TAI", "J2000", "UNIX", "POSIX", "GPS".
1935
+ offset_from : str, Optional
1936
+ Used to reference another time parameter by name. It allows
1937
+ for the stringing together of several dissimilar but related time parameters.
1938
+
1939
+ Notes
1940
+ -----
1941
+ The XTCE spec is not very clear about OffsetFrom or what it is for. We parse it but don't use it for
1942
+ anything.
1943
+ """
1944
+ super().__init__(name, encoding, unit=unit)
1945
+ self.epoch = epoch
1946
+ self.offset_from = offset_from
1947
+
1948
+ @classmethod
1949
+ def from_parameter_type_xml_element(cls, element: ElementTree.Element, ns: dict):
1950
+ """Create a *TimeParameterType from an <xtce:*TimeParameterType> XML element.
1951
+
1952
+ Parameters
1953
+ ----------
1954
+ element : ElementTree.Element
1955
+ The XML element from which to create the object.
1956
+ ns: dict
1957
+ XML namespace dict
1958
+
1959
+ Returns
1960
+ -------
1961
+ : TimeParameterType
1962
+ """
1963
+ name = element.attrib['name']
1964
+ unit = cls.get_units(element, ns)
1965
+ encoding = cls.get_data_encoding(element, ns)
1966
+ encoding_unit_scaler = cls.get_time_unit_linear_scaler(element, ns)
1967
+ if encoding_unit_scaler:
1968
+ encoding.default_calibrator = encoding_unit_scaler
1969
+ epoch = cls.get_epoch(element, ns)
1970
+ offset_from = cls.get_offset_from(element, ns)
1971
+ return cls(name, encoding, unit, epoch, offset_from)
1972
+
1973
+ @staticmethod
1974
+ def get_units(parameter_type_element: ElementTree.Element, ns: dict) -> str or None:
1975
+ """Finds the units associated with a parameter type element and parsed them to return a unit string.
1976
+ We assume only one <xtce:Unit> but this could be extended to support multiple units.
1977
+ See section 4.3.2.2.4 of CCSDS 660.1-G-1
1978
+
1979
+ Parameters
1980
+ ----------
1981
+ parameter_type_element : ElementTree.Element
1982
+ The parameter type element
1983
+ ns : dict
1984
+ XML namespace dictionary
1985
+
1986
+ Returns
1987
+ -------
1988
+ : str or None
1989
+ """
1990
+ encoding_element = parameter_type_element.find('xtce:Encoding', ns)
1991
+ if encoding_element and "units" in encoding_element.attrib:
1992
+ units = encoding_element.attrib["units"]
1993
+ return units
1994
+ # Units are optional so return None if they aren't specified
1995
+ return None
1996
+
1997
+ @staticmethod
1998
+ def get_time_unit_linear_scaler(
1999
+ parameter_type_element: ElementTree.Element, ns: dict) -> PolynomialCalibrator or None:
2000
+ """Finds the linear calibrator associated with the Encoding element for the parameter type element.
2001
+ See section 4.3.2.4.8.3 of CCSDS 660.1-G-2
2002
+
2003
+ Parameters
2004
+ ----------
2005
+ parameter_type_element : ElementTree.Element
2006
+ The parameter type element
2007
+ ns : dict
2008
+ XML namespace dictionary
2009
+
2010
+ Returns
2011
+ -------
2012
+ : PolynomialCalibrator or None
2013
+ """
2014
+ encoding_element = parameter_type_element.find('xtce:Encoding', ns)
2015
+ coefficients = []
2016
+
2017
+ if "offset" in encoding_element.attrib:
2018
+ offset = encoding_element.attrib["offset"]
2019
+ c0 = PolynomialCoefficient(coefficient=float(offset), exponent=0)
2020
+ coefficients.append(c0)
2021
+
2022
+ if "scale" in encoding_element.attrib:
2023
+ scale = encoding_element.attrib["scale"]
2024
+ c1 = PolynomialCoefficient(coefficient=float(scale), exponent=1)
2025
+ coefficients.append(c1)
2026
+ # If we have an offset but not a scale, we need to add a first order term with coefficient 1
2027
+ elif "offset" in encoding_element.attrib:
2028
+ c1 = PolynomialCoefficient(coefficient=1, exponent=1)
2029
+ coefficients.append(c1)
2030
+
2031
+ if coefficients:
2032
+ return PolynomialCalibrator(coefficients=coefficients)
2033
+ # If we didn't find offset nor scale, return None (no calibrator)
2034
+ return None
2035
+
2036
+ @staticmethod
2037
+ def get_epoch(parameter_type_element: ElementTree.Element, ns: dict) -> str or None:
2038
+ """Finds the epoch associated with a parameter type element and parses them to return an epoch string.
2039
+ See section 4.3.2.4.9 of CCSDS 660.1-G-2
2040
+
2041
+ Parameters
2042
+ ----------
2043
+ parameter_type_element : ElementTree.Element
2044
+ The parameter type element
2045
+ ns : dict
2046
+ XML namespace dictionary
2047
+
2048
+ Returns
2049
+ -------
2050
+ : str or None
2051
+ The epoch string, which may be a datetime string or a named epoch such as TAI.
2052
+ """
2053
+ epoch_element = parameter_type_element.find('xtce:ReferenceTime/xtce:Epoch', ns)
2054
+ if epoch_element is not None:
2055
+ return epoch_element.text
2056
+ return None
2057
+
2058
+ @staticmethod
2059
+ def get_offset_from(parameter_type_element: ElementTree.Element, ns: dict) -> str or None:
2060
+ """Finds the parameter referenced in OffsetFrom in a parameter type element and returns the name of the
2061
+ referenced parameter (which must be of type TimeParameterType).
2062
+ See section 4.3.2.4.9 of CCSDS 660.1-G-1
2063
+
2064
+ Parameters
2065
+ ----------
2066
+ parameter_type_element : ElementTree.Element
2067
+ The parameter type element
2068
+ ns : dict
2069
+ XML namespace dictionary
2070
+
2071
+ Returns
2072
+ -------
2073
+ : str or None
2074
+ The named of the referenced parameter.
2075
+ """
2076
+ offset_from_element = parameter_type_element.find('xtce:ReferenceTime/xtce:OffsetFrom', ns)
2077
+ if offset_from_element is not None:
2078
+ return offset_from_element.attrib['parameterRef']
2079
+ return None
2080
+
2081
+
2082
+ class AbsoluteTimeParameterType(TimeParameterType):
2083
+ """<xtce:AbsoluteTimeParameterType>"""
2084
+ pass
2085
+
2086
+
2087
+ class RelativeTimeParameterType(TimeParameterType):
2088
+ """<xtce:RelativeTimeParameterType>"""
2089
+ pass
2090
+
2091
+
2092
+ class Parameter(AttrComparable):
1872
2093
  """<xtce:Parameter>"""
1873
2094
 
1874
- def __init__(self, name: str, parameter_type: ParameterType):
2095
+ def __init__(self, name: str, parameter_type: ParameterType,
2096
+ short_description: str = None, long_description: str = None):
1875
2097
  """Constructor
1876
2098
 
1877
2099
  Parameters
@@ -1880,9 +2102,15 @@ class Parameter:
1880
2102
  Parameter name. Typically something like MSN__PARAMNAME
1881
2103
  parameter_type : ParameterType
1882
2104
  Parameter type object that describes how the parameter is stored.
2105
+ short_description : str
2106
+ Short description of parameter as parsed from XTCE
2107
+ long_description : str
2108
+ Long description of parameter as parsed from XTCE
1883
2109
  """
1884
2110
  self.name = name
1885
2111
  self.parameter_type = parameter_type
2112
+ self.short_description = short_description
2113
+ self.long_description = long_description
1886
2114
 
1887
2115
  def __repr__(self):
1888
2116
  module = self.__class__.__module__
@@ -1890,7 +2118,7 @@ class Parameter:
1890
2118
  return f"<{module}.{qualname} {self.name}>"
1891
2119
 
1892
2120
 
1893
- class SequenceContainer:
2121
+ class SequenceContainer(AttrComparable):
1894
2122
  """<xtce:SequenceContainer>"""
1895
2123
 
1896
2124
  def __init__(self,
@@ -1951,6 +2179,9 @@ class XtcePacketDefinition:
1951
2179
  '{{{xtce}}}FloatParameterType': FloatParameterType,
1952
2180
  '{{{xtce}}}EnumeratedParameterType': EnumeratedParameterType,
1953
2181
  '{{{xtce}}}BinaryParameterType': BinaryParameterType,
2182
+ '{{{xtce}}}BooleanParameterType': BooleanParameterType,
2183
+ '{{{xtce}}}AbsoluteTimeParameterType': AbsoluteTimeParameterType,
2184
+ '{{{xtce}}}RelativeTimeParameterType': RelativeTimeParameterType,
1954
2185
  }
1955
2186
 
1956
2187
  def __init__(self, xtce_document: str or Path, ns: dict = None):
@@ -1970,6 +2201,7 @@ class XtcePacketDefinition:
1970
2201
  """
1971
2202
  self._sequence_container_cache = {} # Lookup for parsed sequence container objects
1972
2203
  self._parameter_cache = {} # Lookup for parsed parameter objects
2204
+ self._parameter_type_cache = {} # Lookup for parsed parameter type objects
1973
2205
  self.ns = ns or self._default_namespace
1974
2206
  self.type_tag_to_object = {k.format(**self.ns): v for k, v in
1975
2207
  self._tag_to_type_template.items()}
@@ -2008,10 +2240,7 @@ class XtcePacketDefinition:
2008
2240
  try:
2009
2241
  base_container, restriction_criteria = self._get_container_base_container(sequence_container)
2010
2242
  base_sequence_container = self.parse_sequence_container_contents(base_container)
2011
- #base_sequence_container.restriction_criteria = restriction_criteria
2012
2243
  base_container_name = base_sequence_container.name
2013
- # Prepend the base container. This is necessary for handling multiple inheritance.
2014
- #entry_list.insert(0, base_sequence_container)
2015
2244
  except ElementNotFoundError:
2016
2245
  base_container_name = None
2017
2246
  restriction_criteria = None
@@ -2028,14 +2257,32 @@ class XtcePacketDefinition:
2028
2257
  else:
2029
2258
  parameter_element = self._find_parameter(parameter_name)
2030
2259
  parameter_type_name = parameter_element.attrib['parameterTypeRef']
2031
- parameter_type_element = self._find_parameter_type(parameter_type_name)
2032
- parameter_type_class = self.type_tag_to_object[parameter_type_element.tag]
2260
+
2261
+ # If we've already parsed this parameter type for a different parameter
2262
+ if parameter_type_name in self._parameter_type_cache:
2263
+ parameter_type_object = self._parameter_type_cache[parameter_type_name]
2264
+ else:
2265
+ parameter_type_element = self._find_parameter_type(parameter_type_name)
2266
+ parameter_type_class = self.type_tag_to_object[parameter_type_element.tag]
2267
+ parameter_type_object = parameter_type_class.from_parameter_type_xml_element(
2268
+ parameter_type_element, self.ns)
2269
+ self._parameter_type_cache[parameter_type_name] = parameter_type_object # Add to cache
2270
+
2271
+ parameter_short_description = parameter_element.attrib['shortDescription'] if (
2272
+ 'shortDescription' in parameter_element.attrib
2273
+ ) else None
2274
+ parameter_long_description = parameter_element.find('xtce:LongDescription', self.ns).text if (
2275
+ parameter_element.find('xtce:LongDescription', self.ns) is not None
2276
+ ) else None
2277
+
2033
2278
  parameter_object = Parameter(
2034
2279
  name=parameter_name,
2035
- parameter_type=parameter_type_class.from_parameter_type_xml_element(
2036
- parameter_type_element, self.ns))
2280
+ parameter_type=parameter_type_object,
2281
+ short_description=parameter_short_description,
2282
+ long_description=parameter_long_description
2283
+ )
2037
2284
  entry_list.append(parameter_object)
2038
- self._parameter_cache[parameter_name] = parameter_object
2285
+ self._parameter_cache[parameter_name] = parameter_object # Add to cache
2039
2286
  elif entry.tag == '{{{xtce}}}ContainerRefEntry'.format(**self.ns): # pylint: disable=consider-using-f-string
2040
2287
  nested_container = self._find_container(name=entry.attrib['containerRef'])
2041
2288
  entry_list.append(self.parse_sequence_container_contents(nested_container))
@@ -2057,9 +2304,19 @@ class XtcePacketDefinition:
2057
2304
 
2058
2305
  @property
2059
2306
  def named_containers(self):
2060
- """Property accessor that returns the dict cache of SequenceContainer objecs"""
2307
+ """Property accessor that returns the dict cache of SequenceContainer objects"""
2061
2308
  return self._sequence_container_cache
2062
2309
 
2310
+ @property
2311
+ def named_parameters(self):
2312
+ """Property accessor that returns the dict cache of Parameter objects"""
2313
+ return self._parameter_cache
2314
+
2315
+ @property
2316
+ def named_parameter_types(self):
2317
+ """Property accessor that returns the dict cache of ParameterType objects"""
2318
+ return self._parameter_type_cache
2319
+
2063
2320
  @property
2064
2321
  def flattened_containers(self):
2065
2322
  """Accesses a flattened, generic representation of non-abstract packet definitions along with their
@@ -1,37 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: space-packet-parser
3
- Version: 4.0.2
4
- Summary: A CCSDS telemetry packet decoding library based on the XTCE packet format description standard.
5
- Home-page: https://github.com/medley56/space_packet_parser
6
- License: BSD-3-Clause
7
- Keywords: ccsds,xtce,space data systems,space packet protocol,packet parsing,lasp,university of colorado,data processing,data extraction,data manipulation,data transformation,data encoding,data decoding,packet inspection,binary data,python
8
- Author: Gavin Medley
9
- Author-email: gavin.medley@lasp.colorado.edu
10
- Maintainer: Gavin Medley
11
- Maintainer-email: gavin.medley@lasp.colorado.edu
12
- Requires-Python: >=3.8,<3.12
13
- Classifier: License :: OSI Approved :: BSD License
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.8
16
- Classifier: Programming Language :: Python :: 3.9
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Requires-Dist: bitstring (>=3.0.0,<5)
20
- Project-URL: Repository, https://github.com/medley56/space_packet_parser
21
- Description-Content-Type: text/markdown
22
-
23
- # Space Packet Parser - a configurable parser for CCSDS telemetry packets
24
- This is a package for decoding CCSDS telemetry packets according to an XTCE or CSV packet structure definition.
25
- It is based on the UML model of the XTCE spec and aims to support all but the most esoteric elements of the
26
- XTCE telemetry packet specification.
27
-
28
- Resources:
29
- - [XTCE (Green Book - Informational Report)](https://public.ccsds.org/Pubs/660x2g2.pdf)
30
- - [XTCE Element Description (Green Book - Informational Report)](https://public.ccsds.org/Pubs/660x1g2.pdf)
31
- - [XTCE (Blue Book - Recommended Standard)](https://public.ccsds.org/Pubs/660x0b2.pdf)
32
-
33
- ## Installation
34
- ```bash
35
- pip install space_packet_parser
36
- ```
37
-
@@ -1,14 +0,0 @@
1
- # Space Packet Parser - a configurable parser for CCSDS telemetry packets
2
- This is a package for decoding CCSDS telemetry packets according to an XTCE or CSV packet structure definition.
3
- It is based on the UML model of the XTCE spec and aims to support all but the most esoteric elements of the
4
- XTCE telemetry packet specification.
5
-
6
- Resources:
7
- - [XTCE (Green Book - Informational Report)](https://public.ccsds.org/Pubs/660x2g2.pdf)
8
- - [XTCE Element Description (Green Book - Informational Report)](https://public.ccsds.org/Pubs/660x1g2.pdf)
9
- - [XTCE (Blue Book - Recommended Standard)](https://public.ccsds.org/Pubs/660x0b2.pdf)
10
-
11
- ## Installation
12
- ```bash
13
- pip install space_packet_parser
14
- ```
@@ -1,30 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- from setuptools import setup
3
-
4
- packages = \
5
- ['space_packet_parser']
6
-
7
- package_data = \
8
- {'': ['*']}
9
-
10
- install_requires = \
11
- ['bitstring>=3.0.0,<5']
12
-
13
- setup_kwargs = {
14
- 'name': 'space-packet-parser',
15
- 'version': '4.0.2',
16
- 'description': 'A CCSDS telemetry packet decoding library based on the XTCE packet format description standard.',
17
- 'long_description': '# Space Packet Parser - a configurable parser for CCSDS telemetry packets\nThis is a package for decoding CCSDS telemetry packets according to an XTCE or CSV packet structure definition. \nIt is based on the UML model of the XTCE spec and aims to support all but the most esoteric elements of the \nXTCE telemetry packet specification.\n\nResources:\n- [XTCE (Green Book - Informational Report)](https://public.ccsds.org/Pubs/660x2g2.pdf)\n- [XTCE Element Description (Green Book - Informational Report)](https://public.ccsds.org/Pubs/660x1g2.pdf)\n- [XTCE (Blue Book - Recommended Standard)](https://public.ccsds.org/Pubs/660x0b2.pdf)\n\n## Installation\n```bash\npip install space_packet_parser\n```\n',
18
- 'author': 'Gavin Medley',
19
- 'author_email': 'gavin.medley@lasp.colorado.edu',
20
- 'maintainer': 'Gavin Medley',
21
- 'maintainer_email': 'gavin.medley@lasp.colorado.edu',
22
- 'url': 'https://github.com/medley56/space_packet_parser',
23
- 'packages': packages,
24
- 'package_data': package_data,
25
- 'install_requires': install_requires,
26
- 'python_requires': '>=3.8,<3.12',
27
- }
28
-
29
-
30
- setup(**setup_kwargs)