Skip to content

Commit 5220c06

Browse files
authored
Merge pull request #84 from us-irs/update-spacepackets-api-and-docs
update CCSDS Spacepackets API and docs
2 parents c5e0017 + 031881e commit 5220c06

File tree

5 files changed

+185
-45
lines changed

5 files changed

+185
-45
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1212

1313
Renamed `PusFileSeqCountProvider` to `CcsdsFileSeqCountProvider` but keep old alias.
1414

15+
## Added
16+
17+
- New `SpacePacketHeader.tc` and `SpacePacketHeader.tm` constructors which set the packet
18+
type correctly
19+
1520
# [v0.24.2] 2024-10-15
1621

1722
## Fixed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ coverage run -m pytest
7474

7575
# Documentation
7676

77-
The documentation is built with Sphinx
77+
The documentation is built with Sphinx and new documentation should be written using the
78+
[NumPy format](https://www.sphinx-doc.org/en/master/usage/extensions/example_numpy.html#example-numpy).
7879

7980
Install the required dependencies first:
8081

docs/examples.rst

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,38 @@
11
Examples
22
=========
33

4-
ECSS PUS packets
5-
-----------------
6-
7-
The following example shows how to generate PUS packets using the PUS ping telecommand and a
8-
PUS ping telemetry reply without a timestamp.
9-
10-
.. testcode:: pus
11-
12-
from spacepackets.ecss.tc import PusTc
13-
from spacepackets.ecss.tm import PusTm
14-
15-
ping_cmd = PusTc(service=17, subservice=1, apid=0x01)
16-
cmd_as_bytes = ping_cmd.pack()
17-
print(f"Ping telecommand [17,1] (hex): [{cmd_as_bytes.hex(sep=',')}]")
18-
19-
ping_reply = PusTm(service=17, subservice=2, apid=0x01, timestamp=bytes())
20-
tm_as_bytes = ping_reply.pack()
21-
print(f"Ping reply [17,2] (hex): [{tm_as_bytes.hex(sep=',')}]")
22-
23-
Output:
24-
25-
.. testoutput:: pus
26-
27-
Ping telecommand [17,1] (hex): [18,01,c0,00,00,06,2f,11,01,00,00,16,1d]
28-
Ping reply [17,2] (hex): [08,01,c0,00,00,08,20,11,02,00,00,00,00,86,d7]
29-
304
CCSDS Space Packet
315
-------------------
326

33-
The following example shows how to generate a space packet header:
7+
The following example shows how to generate a space packet header or a custom telecommand
8+
based on CCSDS space packets.
349

3510
.. testcode:: ccsds
3611

37-
from spacepackets.ccsds.spacepacket import SpHeader, PacketType
12+
from spacepackets.ccsds.spacepacket import SpHeader, PacketType, CCSDS_HEADER_LEN
3813

3914
spacepacket_header = SpHeader(
4015
packet_type=PacketType.TC, apid=0x01, seq_count=0, data_len=0
4116
)
4217
header_as_bytes = spacepacket_header.pack()
4318
print(f"Space packet header (hex): [{header_as_bytes.hex(sep=',')}]")
4419

20+
# Create CCSDS space packet telecommand with custom data.
21+
custom_data = bytes([1, 2, 3, 4])
22+
tc_header = SpHeader.tc(apid=2, seq_count=5, data_len=0)
23+
tc_header.set_data_len_from_packet_len(CCSDS_HEADER_LEN + len(custom_data))
24+
telecommand = tc_header.pack()
25+
telecommand.extend(custom_data)
26+
print(f"Space packet telecommand (hex): [{telecommand.hex(sep=',')}]")
27+
28+
29+
4530
Output:
4631

4732
.. testoutput:: ccsds
4833

4934
Space packet header (hex): [10,01,c0,00,00,00]
35+
Space packet telecommand (hex): [10,02,c0,05,00,03,01,02,03,04]
5036

5137
CFDP Packets
5238
-----------------
@@ -119,6 +105,32 @@ Output
119105
--- PDU 2 RAW ---
120106
0x[24,00,0a,00,01,00,02,04,00,1c,29,1c,a3,00,00,00,0c]
121107

108+
ECSS PUS packets
109+
-----------------
110+
111+
The following example shows how to generate PUS packets using the PUS ping telecommand and a
112+
PUS ping telemetry reply without a timestamp.
113+
114+
.. testcode:: pus
115+
116+
from spacepackets.ecss.tc import PusTc
117+
from spacepackets.ecss.tm import PusTm
118+
119+
ping_cmd = PusTc(service=17, subservice=1, apid=0x01)
120+
cmd_as_bytes = ping_cmd.pack()
121+
print(f"Ping telecommand [17,1] (hex): [{cmd_as_bytes.hex(sep=',')}]")
122+
123+
ping_reply = PusTm(service=17, subservice=2, apid=0x01, timestamp=bytes())
124+
tm_as_bytes = ping_reply.pack()
125+
print(f"Ping reply [17,2] (hex): [{tm_as_bytes.hex(sep=',')}]")
126+
127+
Output:
128+
129+
.. testoutput:: pus
130+
131+
Ping telecommand [17,1] (hex): [18,01,c0,00,00,06,2f,11,01,00,00,16,1d]
132+
Ping reply [17,2] (hex): [08,01,c0,00,00,08,20,11,02,00,00,00,00,86,d7]
133+
122134
USLP Frames
123135
-------------------
124136

spacepackets/ccsds/spacepacket.py

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,7 @@ def pack(self) -> bytearray:
172172

173173

174174
class SpacePacketHeader(AbstractSpacePacket):
175-
"""This class encapsulates the space packet header.
176-
Packet reference: Blue Book CCSDS 133.0-B-2"""
175+
"""This class encapsulates the space packet header. Packet reference: Blue Book CCSDS 133.0-B-2"""
177176

178177
def __init__(
179178
self,
@@ -187,6 +186,10 @@ def __init__(
187186
):
188187
"""Create a space packet header with the given field parameters.
189188
189+
The data length field can also be set from the total packet length by using the
190+
:py:meth:`set_data_len_from_packet_len` method after construction of the space packet
191+
header object.
192+
190193
>>> sph = SpacePacketHeader(packet_type=PacketType.TC, apid=0x42, seq_count=0, data_len=12)
191194
>>> hex(sph.apid)
192195
'0x42'
@@ -201,17 +204,28 @@ def __init__(
201204
>>> sph.packet_seq_control
202205
PacketSeqCtrl(seq_flags=<SequenceFlags.UNSEGMENTED: 3>, seq_count=0)
203206
204-
:param packet_type: 0 for Telemetery, 1 for Telecommands
205-
:param apid: Application Process ID, should not be larger
206-
than 11 bits, deciaml 2074 or hex 0x7ff
207-
:param seq_count: Source sequence counter, should not be larger than 0x3fff or
208-
decimal 16383
209-
:param data_len: Contains a length count C that equals one fewer than the length of the
210-
packet data field. Should not be larger than 65535 bytes
211-
:param ccsds_version:
212-
:param sec_header_flag: Secondary header flag, or False by default.
213-
:param seq_flags:
214-
:raises ValueError: On invalid parameters
207+
Parameters
208+
-----------
209+
packet_type: PacketType
210+
0 for Telemetery, 1 for Telecommands
211+
apid: int
212+
Application Process ID, should not be larger than 11 bits, deciaml 2074 or hex 0x7ff
213+
seq_count: int
214+
Source sequence counter, should not be larger than 0x3fff or decimal 16383
215+
data_len: int
216+
Contains a length count C that equals one fewer than the length of the packet data
217+
field. Should not be larger than 65535 bytes
218+
sec_header_flag: bool
219+
Secondary header flag, or False by default.
220+
seq_flags:
221+
Sequence flags, defaults to unsegmented.
222+
ccsds_version: int
223+
Version of the CCSDS packet. Defaults to 0b000
224+
225+
Raises
226+
--------
227+
ValueError
228+
On invalid parameters
215229
"""
216230
if data_len > pow(2, 16) - 1 or data_len < 0:
217231
raise ValueError(
@@ -225,6 +239,54 @@ def __init__(
225239
self._psc = PacketSeqCtrl(seq_flags=seq_flags, seq_count=seq_count)
226240
self.data_len = data_len
227241

242+
@classmethod
243+
def tc(
244+
cls,
245+
apid: int,
246+
seq_count: int,
247+
data_len: int,
248+
sec_header_flag: bool = False,
249+
seq_flags: SequenceFlags = SequenceFlags.UNSEGMENTED,
250+
ccsds_version: int = 0b000,
251+
) -> SpacePacketHeader:
252+
"""Create a space packet header with the given field parameters for a telecommand packet.
253+
Calls the default constructor :py:meth:`SpacePacketHeader` with the packet type
254+
set to :py:class:`PacketType.TC`.
255+
"""
256+
return cls(
257+
packet_type=PacketType.TC,
258+
apid=apid,
259+
seq_count=seq_count,
260+
data_len=data_len,
261+
sec_header_flag=sec_header_flag,
262+
seq_flags=seq_flags,
263+
ccsds_version=ccsds_version,
264+
)
265+
266+
@classmethod
267+
def tm(
268+
cls,
269+
apid: int,
270+
seq_count: int,
271+
data_len: int,
272+
sec_header_flag: bool = False,
273+
seq_flags: SequenceFlags = SequenceFlags.UNSEGMENTED,
274+
ccsds_version: int = 0b000,
275+
) -> SpacePacketHeader:
276+
"""Create a space packet header with the given field parameters for a telemetry packet.
277+
Calls the default constructor :py:meth:`SpacePacketHeader` with the packet type
278+
set to :py:class:`PacketType.TM`.
279+
"""
280+
return cls(
281+
packet_type=PacketType.TM,
282+
apid=apid,
283+
seq_count=seq_count,
284+
data_len=data_len,
285+
sec_header_flag=sec_header_flag,
286+
seq_flags=seq_flags,
287+
ccsds_version=ccsds_version,
288+
)
289+
228290
@classmethod
229291
def from_composite_fields(
230292
cls,
@@ -289,6 +351,19 @@ def sec_header_flag(self, value):
289351
def seq_count(self):
290352
return self._psc.seq_count
291353

354+
def set_data_len_from_packet_len(self, packet_len: int):
355+
"""Sets the data length field from the given total packet length. The total packet length
356+
must be at least 7 bytes.
357+
358+
Raises
359+
-------
360+
ValueError
361+
The passed packet length is smaller than the minimum expected 7 bytes.
362+
"""
363+
if packet_len < CCSDS_HEADER_LEN + 1:
364+
raise ValueError("specified total packet length too short")
365+
self.data_len = packet_len - CCSDS_HEADER_LEN - 1
366+
292367
@seq_count.setter
293368
def seq_count(self, seq_cnt):
294369
self._psc.seq_count = seq_cnt
@@ -313,7 +388,12 @@ def apid(self, apid):
313388
def packet_len(self) -> int:
314389
"""Retrieve the full space packet size when packed.
315390
316-
:return: Size of the TM packet based on the space packet header data length field.
391+
The full packet size is the data length field plus the :py:const:`CCSDS_HEADER_LEN` of 6
392+
bytes plus one.
393+
394+
Returns
395+
--------
396+
Size of the TM packet based on the space packet header data length field.
317397
"""
318398
return CCSDS_HEADER_LEN + self.data_len + 1
319399

@@ -383,8 +463,12 @@ def __repr__(self):
383463
)
384464

385465
def pack(self) -> bytearray:
386-
"""Pack the raw byte representation of the space packet
387-
:raises ValueError: Mandatory fields were not supplied properly"""
466+
"""Pack the raw byte representation of the space packet.
467+
468+
Raises
469+
--------
470+
ValueError
471+
Mandatory fields were not supplied properly"""
388472
packet = self.sp_header.pack()
389473
if self.sp_header.sec_header_flag:
390474
if self.sec_header is None:

tests/ccsds/test_space_packet.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,44 @@ def test_basic(self):
3535
self.assertEqual(self.sp_header.data_len, 0x16)
3636
self.assertEqual(self.sp_header.packet_type, PacketType.TC)
3737

38+
def test_tm_header(self):
39+
sp_header = SpacePacketHeader.tm(apid=0x03, data_len=16, seq_count=35)
40+
self.assertEqual(sp_header.apid, 0x03)
41+
self.assertEqual(sp_header.seq_flags, SequenceFlags.UNSEGMENTED)
42+
self.assertEqual(sp_header.ccsds_version, 0b000)
43+
self.assertEqual(sp_header.packet_id, PacketId(PacketType.TM, False, 0x03))
44+
self.assertEqual(
45+
sp_header.packet_seq_control,
46+
PacketSeqCtrl(SequenceFlags.UNSEGMENTED, 35),
47+
)
48+
self.assertEqual(sp_header.seq_count, 35)
49+
self.assertEqual(sp_header.data_len, 16)
50+
self.assertEqual(sp_header.packet_type, PacketType.TM)
51+
52+
def test_len_field_setter(self):
53+
self.sp_header.set_data_len_from_packet_len(10)
54+
# Total packet lenght minus the header lenght minus 1
55+
self.assertEqual(self.sp_header.data_len, 3)
56+
57+
def test_invalid_len_field_setter_call(self):
58+
for idx in range(7):
59+
with self.assertRaises(ValueError):
60+
self.sp_header.set_data_len_from_packet_len(idx)
61+
62+
def test_tc_header(self):
63+
sp_header = SpacePacketHeader.tc(apid=0x7FF, data_len=16, seq_count=0x3FFF)
64+
self.assertEqual(sp_header.apid, 0x7FF)
65+
self.assertEqual(sp_header.seq_flags, SequenceFlags.UNSEGMENTED)
66+
self.assertEqual(sp_header.ccsds_version, 0b000)
67+
self.assertEqual(sp_header.packet_id, PacketId(PacketType.TC, False, 0x7FF))
68+
self.assertEqual(
69+
sp_header.packet_seq_control,
70+
PacketSeqCtrl(SequenceFlags.UNSEGMENTED, 0x3FFF),
71+
)
72+
self.assertEqual(sp_header.seq_count, 0x3FFF)
73+
self.assertEqual(sp_header.data_len, 16)
74+
self.assertEqual(sp_header.packet_type, PacketType.TC)
75+
3876
def test_raw_output(self):
3977
raw_output = self.sp_header.pack()
4078
self.assertEqual(

0 commit comments

Comments
 (0)