puslib 0.3.0__tar.gz → 0.4.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.
Files changed (47) hide show
  1. {puslib-0.3.0/src/puslib.egg-info → puslib-0.4.0}/PKG-INFO +11 -12
  2. {puslib-0.3.0 → puslib-0.4.0}/README.md +10 -10
  3. {puslib-0.3.0 → puslib-0.4.0}/setup.cfg +9 -0
  4. puslib-0.4.0/setup.py +3 -0
  5. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/__init__.py +10 -4
  6. puslib-0.4.0/src/puslib/crc_ccitt.py +20 -0
  7. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/ident.py +7 -4
  8. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/packet.py +51 -46
  9. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/parameter.py +75 -64
  10. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/process.py +53 -11
  11. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/pus_policy.py +3 -3
  12. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/services/error_codes.py +2 -0
  13. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/services/param_report.py +6 -5
  14. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/services/pus_001_request_verification.py +9 -9
  15. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/services/pus_003_housekeeping.py +45 -24
  16. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/services/pus_005_event_reporting.py +11 -10
  17. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/services/pus_008_function_management.py +8 -8
  18. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/services/pus_017_test.py +3 -4
  19. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/services/pus_020_parameter_management.py +7 -6
  20. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/services/service.py +17 -8
  21. puslib-0.4.0/src/puslib/streams/buffer.py +34 -0
  22. puslib-0.4.0/src/puslib/streams/console.py +10 -0
  23. puslib-0.4.0/src/puslib/streams/file.py +56 -0
  24. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/streams/stream.py +12 -1
  25. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/time.py +29 -25
  26. {puslib-0.3.0 → puslib-0.4.0/src/puslib.egg-info}/PKG-INFO +11 -12
  27. puslib-0.4.0/tests/test_ident.py +12 -0
  28. {puslib-0.3.0 → puslib-0.4.0}/tests/test_packet.py +64 -0
  29. puslib-0.3.0/setup.py +0 -9
  30. puslib-0.3.0/src/puslib/crc_ccitt.py +0 -44
  31. puslib-0.3.0/src/puslib/streams/buffer.py +0 -23
  32. puslib-0.3.0/src/puslib/streams/console.py +0 -6
  33. puslib-0.3.0/src/puslib/streams/file.py +0 -32
  34. puslib-0.3.0/tests/test_ident.py +0 -12
  35. {puslib-0.3.0 → puslib-0.4.0}/LICENSE +0 -0
  36. {puslib-0.3.0 → puslib-0.4.0}/pyproject.toml +0 -0
  37. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/exceptions.py +0 -0
  38. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/services/__init__.py +0 -0
  39. {puslib-0.3.0 → puslib-0.4.0}/src/puslib/streams/__init__.py +0 -0
  40. {puslib-0.3.0 → puslib-0.4.0}/src/puslib.egg-info/SOURCES.txt +0 -0
  41. {puslib-0.3.0 → puslib-0.4.0}/src/puslib.egg-info/dependency_links.txt +0 -0
  42. {puslib-0.3.0 → puslib-0.4.0}/src/puslib.egg-info/requires.txt +0 -0
  43. {puslib-0.3.0 → puslib-0.4.0}/src/puslib.egg-info/top_level.txt +0 -0
  44. {puslib-0.3.0 → puslib-0.4.0}/tests/test_crc.py +0 -0
  45. {puslib-0.3.0 → puslib-0.4.0}/tests/test_parameter.py +0 -0
  46. {puslib-0.3.0 → puslib-0.4.0}/tests/test_policy.py +0 -0
  47. {puslib-0.3.0 → puslib-0.4.0}/tests/test_time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: puslib
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: An implementation of the telemetry and telecommand packet utilization standard (PUS).
5
5
  Home-page: https://github.com/pxntus/puslib
6
6
  Author: Pontus Nyman
@@ -26,24 +26,23 @@ Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
27
  Requires-Dist: bitstring
28
28
  Dynamic: license-file
29
- Dynamic: requires-dist
30
29
 
31
30
  # Puslib
32
31
 
33
- **Puslib** is a Python implementation of the telemetry and telecommand packet utilization standard (PUS). It is a standard widely used in the space industry for the purposes of remote monitoring and control of spacecraft subsystems and payloads.
32
+ **Puslib** is a Python library for working with the ECSS Packet Utilization Standard (PUS), a protocol widely used in the space industry.
34
33
 
35
- This package is mainly intended for ground segment tools, e.g.:
34
+ PUS defines an application-level interface between ground and space, covering both segments. It specifies a binary packet format for telecommands and telemetry (based on CCSDS space packets) and a routing mechanism for those packets, as well as a software architecture built around application processes and a set of standard services.
36
35
 
37
- * data extraction and post-processing of telemetry.
38
- * simulators.
39
- * EGSE SW, and thus enabling the use of mission control systems for controling and monitoring of EGSE equipment.
40
- * enable Python based commanding for mission control systems and other PUS based systems.
41
- * mission-specific tools.
42
- * student projects.
36
+ Puslib covers two main use cases:
37
+
38
+ * **TM/TC packet parsing** — deserialize raw packet streams, inspect packet fields, and extract parameter data for post-processing or analysis.
39
+ * **PUS application stack** — implement a PUS-compliant backend that receives telecommands and emits telemetry, for use in EGSE equipment, simulators, or any system that communicates over TM/TC.
40
+
41
+ The PUS stack is designed from the perspective of the system being monitored and controlled. It receives telecommands and produces telemetry, making it better suited for simulators and EGSE than for mission control systems. It targets ground tooling and test equipment rather than flight software, but don't let the sky be the limit.
43
42
 
44
43
  ## Install
45
44
 
46
- ```Shell
45
+ ```shell
47
46
  pip install puslib
48
47
  ```
49
48
 
@@ -51,7 +50,7 @@ Python 3.10 or later is required.
51
50
 
52
51
  ## Basic Example
53
52
 
54
- ```Python
53
+ ```python
55
54
  from datetime import datetime
56
55
  from functools import partial
57
56
 
@@ -1,19 +1,19 @@
1
1
  # Puslib
2
2
 
3
- **Puslib** is a Python implementation of the telemetry and telecommand packet utilization standard (PUS). It is a standard widely used in the space industry for the purposes of remote monitoring and control of spacecraft subsystems and payloads.
3
+ **Puslib** is a Python library for working with the ECSS Packet Utilization Standard (PUS), a protocol widely used in the space industry.
4
4
 
5
- This package is mainly intended for ground segment tools, e.g.:
5
+ PUS defines an application-level interface between ground and space, covering both segments. It specifies a binary packet format for telecommands and telemetry (based on CCSDS space packets) and a routing mechanism for those packets, as well as a software architecture built around application processes and a set of standard services.
6
6
 
7
- * data extraction and post-processing of telemetry.
8
- * simulators.
9
- * EGSE SW, and thus enabling the use of mission control systems for controling and monitoring of EGSE equipment.
10
- * enable Python based commanding for mission control systems and other PUS based systems.
11
- * mission-specific tools.
12
- * student projects.
7
+ Puslib covers two main use cases:
8
+
9
+ * **TM/TC packet parsing** — deserialize raw packet streams, inspect packet fields, and extract parameter data for post-processing or analysis.
10
+ * **PUS application stack** — implement a PUS-compliant backend that receives telecommands and emits telemetry, for use in EGSE equipment, simulators, or any system that communicates over TM/TC.
11
+
12
+ The PUS stack is designed from the perspective of the system being monitored and controlled. It receives telecommands and produces telemetry, making it better suited for simulators and EGSE than for mission control systems. It targets ground tooling and test equipment rather than flight software, but don't let the sky be the limit.
13
13
 
14
14
  ## Install
15
15
 
16
- ```Shell
16
+ ```shell
17
17
  pip install puslib
18
18
  ```
19
19
 
@@ -21,7 +21,7 @@ Python 3.10 or later is required.
21
21
 
22
22
  ## Basic Example
23
23
 
24
- ```Python
24
+ ```python
25
25
  from datetime import datetime
26
26
  from functools import partial
27
27
 
@@ -31,6 +31,8 @@ package_dir = = src
31
31
  packages = find:
32
32
  include_package_data = True
33
33
  python_requires = >=3.10
34
+ install_requires =
35
+ bitstring
34
36
 
35
37
  [options.packages.find]
36
38
  where = src
@@ -38,6 +40,13 @@ where = src
38
40
  [tool:pytest]
39
41
  testpaths = tests
40
42
 
43
+ [mypy]
44
+ files = src/puslib
45
+ python_version = 3.10
46
+ warn_return_any = True
47
+ warn_unused_ignores = True
48
+ check_untyped_defs = True
49
+
41
50
  [egg_info]
42
51
  tag_build =
43
52
  tag_date = 0
puslib-0.4.0/setup.py ADDED
@@ -0,0 +1,3 @@
1
+ from setuptools import setup
2
+
3
+ setup()
@@ -2,7 +2,7 @@ from puslib.packet import PusTcPacket, PusTmPacket, AckFlag # noqa: F401
2
2
  from puslib.time import CucTime # noqa: F401
3
3
  from puslib.pus_policy import PusPolicy
4
4
 
5
- __version__ = "0.3.0"
5
+ __version__ = "0.4.0"
6
6
 
7
7
  _pus_policy = PusPolicy()
8
8
 
@@ -14,8 +14,14 @@ def set_policy(policy):
14
14
  e.g., various field lengths. This is due to that many parts of the PUS standard
15
15
  are mission-specific.
16
16
 
17
- The PUS policy is set on a global scope, thus, multiple simultaneous PUS policies
18
- are not supported. Might need to be reviewed in the future.
17
+ The policy is stored as a module-level global and is not thread-safe. This is a
18
+ deliberate design choice: the library assumes a single-threaded execution model
19
+ where the policy is set once at startup and remains unchanged for the lifetime of
20
+ the application. Calling set_policy() from multiple threads, or changing the policy
21
+ while packets are being processed, results in undefined behavior.
22
+
23
+ If per-thread policies are ever required, replace this global with a
24
+ contextvars.ContextVar to give each thread or async task its own policy instance.
19
25
 
20
26
  Arguments:
21
27
  policy -- PUS policy
@@ -24,7 +30,7 @@ def set_policy(policy):
24
30
  _pus_policy = policy
25
31
 
26
32
 
27
- def get_policy():
33
+ def get_policy() -> PusPolicy:
28
34
  """Get the current selected PUS policy.
29
35
 
30
36
  Returns:
@@ -0,0 +1,20 @@
1
+ """CRC-16-CCITT checksum (polynomial 0x1021, preset 0xFFFF)."""
2
+
3
+ import binascii
4
+
5
+ _PRESET = 0xffff
6
+
7
+
8
+ def calculate(buffer: bytes | bytearray | memoryview) -> int:
9
+ """Calculate the CRC-16-CCITT checksum on a buffer.
10
+
11
+ Using polynomial 0x1021 (default for crc_hqx) and preset (or initial value)
12
+ 0xffff according to ECSS and CCSDS standards.
13
+
14
+ Arguments:
15
+ buffer -- input buffer to calculate checksum for
16
+
17
+ Returns:
18
+ 16 bit CRC checksum
19
+ """
20
+ return binascii.crc_hqx(buffer, _PRESET)
@@ -1,4 +1,7 @@
1
- def _seq_count_generator():
1
+ from typing import Iterator
2
+
3
+
4
+ def _seq_count_generator() -> Iterator[int]:
2
5
  seq_count = -1
3
6
  max_count = 2 ** 14
4
7
  while True:
@@ -14,7 +17,7 @@ class PusIdent:
14
17
  - APID (Application ID)
15
18
  - packet sequence count
16
19
 
17
- The sequence count is per APID and spans [0..32767].
20
+ The sequence count is per APID and spans [0..16383].
18
21
 
19
22
  The APID-sequence pair, thus, uniquely identifies a PUS packet for the foreseeable future.
20
23
  """
@@ -33,10 +36,10 @@ class PusIdent:
33
36
  def apid(self) -> int:
34
37
  return self._apid
35
38
 
36
- def seq_count(self) -> int:
39
+ def next_seq_count(self) -> int:
37
40
  """Returns next sequence count.
38
41
 
39
42
  Returns:
40
- sequence count [0..32767]
43
+ sequence count [0..16383]
41
44
  """
42
45
  return next(self._seq_count_generator)
@@ -10,11 +10,10 @@ Implementations of the following packet formats:
10
10
  import struct
11
11
  from enum import IntEnum, IntFlag
12
12
  from dataclasses import dataclass
13
- from typing import Optional, SupportsBytes
14
-
13
+ from typing import Optional, cast
14
+ from puslib.crc_ccitt import calculate as crc_ccitt_calculate
15
15
  from puslib.exceptions import CrcException, IncompletePacketException, InvalidPacketException
16
16
  from puslib.time import CucTime
17
- from puslib.crc_ccitt import calculate as crc_ccitt_calculate
18
17
 
19
18
  _CCSDS_PACKET_VERSION_NUMBER = 0
20
19
  _CCSDS_MAX_PACKET_SIZE = 65542
@@ -26,15 +25,15 @@ _COMMON_SEC_HDR_STRUCT = struct.Struct('>BBB')
26
25
  _PEC_FIELD_SIZE = 2
27
26
 
28
27
 
29
- def _validate_int_field(field_name: str, val: int, min_val: int, max_val: int):
30
- if isinstance(val, int):
28
+ def _validate_int_field(field_name: str, val: int, min_val: int, max_val: int) -> None:
29
+ if isinstance(val, int) and not isinstance(val, bool):
31
30
  if not min_val <= val <= max_val:
32
31
  raise InvalidPacketException(f"{field_name} must be between {min_val} and {max_val}")
33
32
  else:
34
33
  raise TypeError(f"{field_name} must be an integer")
35
34
 
36
35
 
37
- def _validate_bool_field(field_name: str, val: bool):
36
+ def _validate_bool_field(field_name: str, val: bool) -> None:
38
37
  if not isinstance(val, bool):
39
38
  raise TypeError(f"{field_name} must be a bool")
40
39
 
@@ -81,8 +80,8 @@ class CcsdsSpacePacket:
81
80
  has_pec -- true if a PEC field should be present (default: {True})
82
81
  """
83
82
  self.header: _PacketPrimaryHeader = _PacketPrimaryHeader()
84
- self.secondary_header = None
85
- self.payload: SupportsBytes | None = None
83
+ self.secondary_header: _PacketSecondaryHeaderTc | _PacketSecondaryHeaderTm | None = None
84
+ self.payload: bytes | None = None
86
85
  self._has_pec = has_pec
87
86
 
88
87
  def __len__(self):
@@ -109,7 +108,7 @@ class CcsdsSpacePacket:
109
108
  def has_pec(self) -> bool:
110
109
  return self._has_pec
111
110
 
112
- def serialize(self) -> SupportsBytes:
111
+ def serialize(self) -> bytes:
113
112
  """Serialize the packet to its binary format.
114
113
 
115
114
  Returns:
@@ -121,17 +120,17 @@ class CcsdsSpacePacket:
121
120
  if self.header.secondary_header_flag:
122
121
  packet_data_field = b'' # Leave it to subclasses to handle data field
123
122
  else:
124
- packet_data_field = self.payload
123
+ packet_data_field = self.payload or b''
125
124
  return ccsds_header + packet_data_field
126
125
 
127
- def request_id(self) -> SupportsBytes:
126
+ def request_id(self) -> bytes:
128
127
  packet_id = self.header.packet_version_number << 13 | (1 if self.header.packet_type == PacketType.TC else 0) << 12 | (1 if self.header.secondary_header_flag else 0) << 11 | self.header.apid
129
128
  seq_ctrl = self.header.seq_flags << 14 | self.header.seq_count_or_name
130
129
  return struct.pack('>HH', packet_id, seq_ctrl)
131
130
 
132
131
  @classmethod
133
- def deserialize(cls, buffer: SupportsBytes, has_pec: bool = True, validate_pec: bool = True) -> "CcsdsSpacePacket":
134
- """Deserialize a packet from its binary format to a packet object.
132
+ def _parse_primary_header(cls, buffer: bytes | bytearray | memoryview, has_pec: bool = True, validate_pec: bool = True) -> tuple[int, PacketType, bool, int, SequenceFlag, int, int]:
133
+ """Parse and validate the CCSDS primary header from a buffer.
135
134
 
136
135
  Arguments:
137
136
  buffer -- packet in binary format
@@ -141,9 +140,12 @@ class CcsdsSpacePacket:
141
140
  validate_pec -- true if PEC field should be verified during operation (default: {True})
142
141
 
143
142
  Returns:
144
- packet object
143
+ tuple of (packet_version_number, packet_type, secondary_header_flag, apid, seq_flags, seq_count_or_name, data_length)
145
144
  """
146
- packet_id, seq_ctrl, data_length = cls._CCSDS_HDR_STRUCT.unpack_from(buffer)
145
+ try:
146
+ packet_id, seq_ctrl, data_length = cls._CCSDS_HDR_STRUCT.unpack_from(buffer)
147
+ except struct.error:
148
+ raise IncompletePacketException()
147
149
 
148
150
  packet_size = cls._CCSDS_HDR_STRUCT.size + data_length + 1
149
151
  if packet_size > len(buffer):
@@ -156,10 +158,10 @@ class CcsdsSpacePacket:
156
158
  raise CrcException
157
159
 
158
160
  packet_version_number = (packet_id >> 13) & 0b111
159
- packet_type = PacketType.TC if (packet_id >> 12) & 0b1 == 1 else PacketType.TM
160
- secondary_header_flag = True if (packet_id >> 11) & 0b1 == 1 else False
161
+ packet_type = PacketType((packet_id >> 12) & 0b1)
162
+ secondary_header_flag = bool((packet_id >> 11) & 0b1)
161
163
  apid = packet_id & 0x7ff
162
- seq_flags = (seq_ctrl >> 14) & 0b11
164
+ seq_flags = SequenceFlag((seq_ctrl >> 14) & 0b11)
163
165
  seq_count_or_name = seq_ctrl & 0x3fff # count or name
164
166
  return packet_version_number, packet_type, secondary_header_flag, apid, seq_flags, seq_count_or_name, data_length
165
167
 
@@ -199,6 +201,10 @@ class CcsdsSpacePacket:
199
201
  packet.header.apid = apid
200
202
 
201
203
  seq_flags = kwargs.get('seq_flags', SequenceFlag.UNSEGMENTED)
204
+ try:
205
+ seq_flags = SequenceFlag(seq_flags)
206
+ except ValueError:
207
+ raise ValueError(f"seq_flags must be a valid SequenceFlag value (0–3), got {seq_flags!r}")
202
208
  packet.header.seq_flags = seq_flags
203
209
 
204
210
  seq_count = kwargs.get('seq_count_or_name', 0)
@@ -209,7 +215,7 @@ class CcsdsSpacePacket:
209
215
  if isinstance(data, (bytearray, bytes, type(None))):
210
216
  if data is None:
211
217
  data = bytes()
212
- packet.payload = data
218
+ packet.payload = bytes(data)
213
219
  else:
214
220
  raise TypeError("Application data must be None, bytes or a bytearray")
215
221
 
@@ -257,7 +263,7 @@ class PusTcPacket(CcsdsSpacePacket):
257
263
 
258
264
  def __init__(self, has_pec: bool = True):
259
265
  super().__init__(has_pec)
260
- self.secondary_header = _PacketSecondaryHeaderTc()
266
+ self.secondary_header: _PacketSecondaryHeaderTc = _PacketSecondaryHeaderTc()
261
267
 
262
268
  def __len__(self):
263
269
  size = super().__len__()
@@ -277,7 +283,7 @@ class PusTcPacket(CcsdsSpacePacket):
277
283
  return pkt_info
278
284
 
279
285
  @property
280
- def name(self) -> str:
286
+ def name(self) -> int:
281
287
  return self.header.seq_count_or_name
282
288
 
283
289
  def ack(self, ack_flag: AckFlag) -> bool:
@@ -304,10 +310,10 @@ class PusTcPacket(CcsdsSpacePacket):
304
310
  return self.secondary_header.source
305
311
 
306
312
  @property
307
- def app_data(self) -> SupportsBytes:
313
+ def app_data(self) -> bytes | None:
308
314
  return self.payload
309
315
 
310
- def serialize(self):
316
+ def serialize(self) -> bytes:
311
317
  ccsds_header = super().serialize()
312
318
 
313
319
  if self.header.secondary_header_flag:
@@ -326,18 +332,17 @@ class PusTcPacket(CcsdsSpacePacket):
326
332
  ccsds_sec_header_static = bytes()
327
333
 
328
334
  # Packet error control
329
- packet_without_pec = ccsds_header + (ccsds_sec_header_static + ccsds_sec_header_source if self.header.secondary_header_flag else bytes()) + (self.payload if self.header.secondary_header_flag else bytes())
335
+ packet_without_pec = ccsds_header + (ccsds_sec_header_static + ccsds_sec_header_source if self.header.secondary_header_flag else bytes()) + (self.payload or b'' if self.header.secondary_header_flag else bytes())
330
336
  if self.has_pec:
331
337
  mem_view = memoryview(packet_without_pec)
332
- pec = crc_ccitt_calculate(mem_view)
333
- pec = pec.to_bytes(_PEC_FIELD_SIZE, byteorder='big')
338
+ pec = crc_ccitt_calculate(mem_view).to_bytes(_PEC_FIELD_SIZE, byteorder='big')
334
339
  else:
335
340
  pec = bytes()
336
341
 
337
342
  return packet_without_pec + pec
338
343
 
339
344
  @classmethod
340
- def deserialize(cls, buffer: SupportsBytes, has_pec: bool = True, validate_pec: bool = True, has_source_field: bool = True, validate_fields: bool = True):
345
+ def deserialize(cls, buffer: bytes | bytearray | memoryview, has_pec: bool = True, validate_pec: bool = True, has_source_field: bool = True, validate_fields: bool = True) -> "PusTcPacket":
341
346
  """Deserialize a packet from its binary format to a packet object.
342
347
 
343
348
  Arguments:
@@ -352,7 +357,7 @@ class PusTcPacket(CcsdsSpacePacket):
352
357
  Returns:
353
358
  packet object
354
359
  """
355
- packet_version_number, packet_type, secondary_header_flag, apid, seq_flags, seq_count_or_name, data_length = super(cls, cls).deserialize(buffer, has_pec, validate_pec)
360
+ packet_version_number, packet_type, secondary_header_flag, apid, seq_flags, seq_count_or_name, data_length = cls._parse_primary_header(buffer, has_pec, validate_pec)
356
361
  offset = cls._CCSDS_HDR_STRUCT.size
357
362
 
358
363
  data_field_except_source_length = ((_COMMON_SEC_HDR_STRUCT.size + (cls._SOURCE_FIELD_SIZE if has_source_field else 0)) if secondary_header_flag else 0) + (2 if has_pec else 0)
@@ -409,7 +414,8 @@ class PusTcPacket(CcsdsSpacePacket):
409
414
  packet.header.data_length = data_length
410
415
  packet.payload = app_data
411
416
  packet.secondary_header.pus_version = pus_version
412
- packet.secondary_header.ack_flags = ack_flags
417
+ if ack_flags is not None:
418
+ packet.secondary_header.ack_flags = ack_flags
413
419
  packet.secondary_header.service_type = service_type
414
420
  packet.secondary_header.service_subtype = service_subtype
415
421
  packet.secondary_header.source = source
@@ -417,7 +423,7 @@ class PusTcPacket(CcsdsSpacePacket):
417
423
  return packet
418
424
 
419
425
  @classmethod
420
- def create(cls, **kwargs):
426
+ def create(cls, **kwargs) -> "PusTcPacket":
421
427
  """A factory method to create a PUS TC packet instance.
422
428
 
423
429
  Keyword Arguments:
@@ -444,7 +450,7 @@ class PusTcPacket(CcsdsSpacePacket):
444
450
  kwargs['secondary_header_length'] = secondary_header_length
445
451
  kwargs['packet_type'] = PacketType.TC
446
452
  kwargs['seq_count_or_name'] = kwargs.get('name', 0)
447
- packet = super(cls, cls).create(**kwargs)
453
+ packet = super().create(**kwargs)
448
454
 
449
455
  if packet.header.secondary_header_flag:
450
456
  pus_version = kwargs.get('pus_version', _TC_PACKET_PUS_VERSION_NUMBER)
@@ -471,7 +477,7 @@ class PusTcPacket(CcsdsSpacePacket):
471
477
  _validate_int_field('Source ID', source, 0, 0xffff)
472
478
  packet.secondary_header.source = source
473
479
 
474
- return packet
480
+ return cast("PusTcPacket", packet)
475
481
 
476
482
 
477
483
  @dataclass(slots=True)
@@ -493,7 +499,7 @@ class PusTmPacket(CcsdsSpacePacket):
493
499
 
494
500
  def __init__(self, has_pec: bool = True):
495
501
  super().__init__(has_pec)
496
- self.secondary_header = _PacketSecondaryHeaderTm()
502
+ self.secondary_header: _PacketSecondaryHeaderTm = _PacketSecondaryHeaderTm()
497
503
 
498
504
  def __len__(self):
499
505
  size = super().__len__()
@@ -503,7 +509,7 @@ class PusTmPacket(CcsdsSpacePacket):
503
509
  size += self._MSG_TYPE_COUNTER_FIELD_SIZE
504
510
  if self.secondary_header.destination is not None:
505
511
  size += self._DESTINATION_FIELD_SIZE
506
- size += len(self.secondary_header.time)
512
+ size += len(self.secondary_header.time) if self.secondary_header.time else 0
507
513
  return size
508
514
 
509
515
  def __str__(self):
@@ -537,14 +543,14 @@ class PusTmPacket(CcsdsSpacePacket):
537
543
  return self.secondary_header.destination
538
544
 
539
545
  @property
540
- def time(self) -> CucTime:
546
+ def time(self) -> CucTime | None:
541
547
  return self.secondary_header.time
542
548
 
543
549
  @property
544
- def source_data(self) -> SupportsBytes:
550
+ def source_data(self) -> bytes | None:
545
551
  return self.payload
546
552
 
547
- def serialize(self):
553
+ def serialize(self) -> bytes:
548
554
  ccsds_header = super().serialize()
549
555
 
550
556
  # First static part of secondary header
@@ -563,18 +569,17 @@ class PusTmPacket(CcsdsSpacePacket):
563
569
  ccsds_sec_header_destination = bytes()
564
570
 
565
571
  # Packet error control
566
- packet_without_pec = ccsds_header + ccsds_sec_header_static + ccsds_sec_header_msg_type_counter + ccsds_sec_header_destination + bytes(self.secondary_header.time) + self.payload
572
+ packet_without_pec = ccsds_header + ccsds_sec_header_static + ccsds_sec_header_msg_type_counter + ccsds_sec_header_destination + (bytes(self.secondary_header.time) if self.secondary_header.time else b'') + (self.payload or b'')
567
573
  if self.has_pec:
568
574
  mem_view = memoryview(packet_without_pec)
569
- pec = crc_ccitt_calculate(mem_view)
570
- pec = pec.to_bytes(_PEC_FIELD_SIZE, byteorder='big')
575
+ pec = crc_ccitt_calculate(mem_view).to_bytes(_PEC_FIELD_SIZE, byteorder='big')
571
576
  else:
572
577
  pec = bytes()
573
578
 
574
579
  return packet_without_pec + pec
575
580
 
576
581
  @classmethod
577
- def deserialize(cls, buffer: SupportsBytes, has_pec: bool = True, validate_pec: bool = True, cuc_time: CucTime | None = None, has_type_counter_field: bool = True, has_destination_field: bool = True, validate_fields: bool = True):
582
+ def deserialize(cls, buffer: bytes | bytearray | memoryview, has_pec: bool = True, validate_pec: bool = True, cuc_time: CucTime | None = None, has_type_counter_field: bool = True, has_destination_field: bool = True, validate_fields: bool = True) -> "PusTmPacket":
578
583
  """Deserialize a packet from its binary format to a packet object.
579
584
 
580
585
  Arguments:
@@ -591,7 +596,7 @@ class PusTmPacket(CcsdsSpacePacket):
591
596
  Returns:
592
597
  packet object
593
598
  """
594
- packet_version_number, packet_type, secondary_header_flag, apid, seq_flags, seq_count_or_name, data_length = super(cls, cls).deserialize(buffer, has_pec, validate_pec)
599
+ packet_version_number, packet_type, secondary_header_flag, apid, seq_flags, seq_count_or_name, data_length = cls._parse_primary_header(buffer, has_pec, validate_pec)
595
600
  offset = cls._CCSDS_HDR_STRUCT.size
596
601
 
597
602
  if secondary_header_flag:
@@ -682,7 +687,7 @@ class PusTmPacket(CcsdsSpacePacket):
682
687
  return packet
683
688
 
684
689
  @classmethod
685
- def create(cls, **kwargs):
690
+ def create(cls, **kwargs) -> "PusTmPacket":
686
691
  """A factory method to create a PUS TM packet instance.
687
692
 
688
693
  Keyword Arguments:
@@ -709,11 +714,11 @@ class PusTmPacket(CcsdsSpacePacket):
709
714
  time = kwargs.get('time', None)
710
715
 
711
716
  if kwargs.get('secondary_header_flag', True):
712
- secondary_header_length = _COMMON_SEC_HDR_STRUCT.size + (2 if msg_type_counter is not None else 0) + (2 if destination is not None else 0) + len(time)
717
+ secondary_header_length = _COMMON_SEC_HDR_STRUCT.size + (2 if msg_type_counter is not None else 0) + (2 if destination is not None else 0) + (len(time) if time else 0)
713
718
  kwargs['secondary_header_length'] = secondary_header_length
714
719
  kwargs['packet_type'] = PacketType.TM
715
720
  kwargs['seq_count_or_name'] = kwargs.get('seq_count', 0)
716
- packet = super(cls, cls).create(**kwargs)
721
+ packet = super().create(**kwargs)
717
722
 
718
723
  if packet.header.secondary_header_flag:
719
724
  pus_version = kwargs.get('pus_version', _TM_PACKET_PUS_VERSION_NUMBER)
@@ -746,4 +751,4 @@ class PusTmPacket(CcsdsSpacePacket):
746
751
  if time:
747
752
  packet.secondary_header.time = time
748
753
 
749
- return packet
754
+ return cast("PusTmPacket", packet)