fprime-gds 4.0.0a1__py3-none-any.whl → 4.0.0a3__py3-none-any.whl

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.
@@ -59,7 +59,7 @@ class SerialAdapter(fprime_gds.common.communication.adapters.base.BaseAdapter):
59
59
  4000000,
60
60
  ]
61
61
 
62
- def __init__(self, device, baud):
62
+ def __init__(self, device, baud, skip_check):
63
63
  """
64
64
  Initialize the serial adapter using the default settings. This does not open the serial port, but sets up all
65
65
  the internal variables used when opening the device.
@@ -77,9 +77,14 @@ class SerialAdapter(fprime_gds.common.communication.adapters.base.BaseAdapter):
77
77
  Opens the serial port based on previously supplied settings. If the port is already open, then close it first.
78
78
  Then open the port up again.
79
79
  """
80
- self.close()
81
- self.serial = serial.Serial(self.device, self.baud)
82
- return self.serial is not None
80
+ try:
81
+ self.close()
82
+ self.serial = serial.Serial(self.device, self.baud)
83
+ return self.serial is not None
84
+ except serial.serialutil.SerialException as exc:
85
+ LOGGER.warning("Serial exception caught: %s. Reconnecting.", (str(exc)))
86
+ self.close()
87
+ return False
83
88
 
84
89
  def close(self):
85
90
  """
@@ -100,12 +105,11 @@ class SerialAdapter(fprime_gds.common.communication.adapters.base.BaseAdapter):
100
105
  :return: True, when data was sent through the UART. False otherwise.
101
106
  """
102
107
  try:
103
- if self.serial is None:
104
- self.open()
105
- written = self.serial.write(frame)
106
- # Not believed to be possible to not send everything without getting a timeout exception
107
- assert written == len(frame)
108
- return True
108
+ if self.serial is not None or self.open():
109
+ written = self.serial.write(frame)
110
+ # Not believed to be possible to not send everything without getting a timeout exception
111
+ assert written == len(frame)
112
+ return True
109
113
  except serial.serialutil.SerialException as exc:
110
114
  LOGGER.warning("Serial exception caught: %s. Reconnecting.", (str(exc)))
111
115
  self.close()
@@ -121,17 +125,16 @@ class SerialAdapter(fprime_gds.common.communication.adapters.base.BaseAdapter):
121
125
  """
122
126
  data = b""
123
127
  try:
124
- if self.serial is None:
125
- self.open()
126
- # Read as much data as possible, while ensuring to block if no data is available at this time. Note: as much
127
- # data is read as possible to avoid a long-return time to this call. Minimum data to read is one byte in
128
- # order to block this function while data is incoming.
129
- self.serial.timeout = timeout
130
- data = self.serial.read(1) # Force a block for at least 1 character
131
- while self.serial.in_waiting:
132
- data += self.serial.read(
133
- self.serial.in_waiting
134
- ) # Drain the incoming data queue
128
+ if self.serial is not None or self.open():
129
+ # Read as much data as possible, while ensuring to block if no data is available at this time. Note: as much
130
+ # data is read as possible to avoid a long-return time to this call. Minimum data to read is one byte in
131
+ # order to block this function while data is incoming.
132
+ self.serial.timeout = timeout
133
+ data = self.serial.read(1) # Force a block for at least 1 character
134
+ while self.serial.in_waiting:
135
+ data += self.serial.read(
136
+ self.serial.in_waiting
137
+ ) # Drain the incoming data queue
135
138
  except serial.serialutil.SerialException as exc:
136
139
  LOGGER.warning("Serial exception caught: %s. Reconnecting.", (str(exc)))
137
140
  self.close()
@@ -161,6 +164,12 @@ class SerialAdapter(fprime_gds.common.communication.adapters.base.BaseAdapter):
161
164
  "default": 9600,
162
165
  "help": "Baud rate of the serial device.",
163
166
  },
167
+ ("--uart-skip-port-check",): {
168
+ "dest": "skip_check",
169
+ "default": False,
170
+ "action": "store_true",
171
+ "help": "Skip checking that the port exists"
172
+ }
164
173
  }
165
174
 
166
175
  @classmethod
@@ -175,20 +184,20 @@ class SerialAdapter(fprime_gds.common.communication.adapters.base.BaseAdapter):
175
184
  return cls
176
185
 
177
186
  @classmethod
178
- def check_arguments(cls, device, baud):
187
+ def check_arguments(cls, device, baud, skip_check):
179
188
  """
180
189
  Code that should check arguments of this adapter. If there is a problem with this code, then a "ValueError"
181
190
  should be raised describing the problem with these arguments.
182
191
 
183
192
  :param args: arguments as dictionary
184
193
  """
185
- ports = map(lambda info: info.device, list_ports.comports(include_links=True))
186
- if device not in ports:
194
+ ports = list(map(lambda info: info.device, list_ports.comports(include_links=True)))
195
+ if not skip_check and device not in ports:
187
196
  msg = f"Serial port '{device}' not valid. Available ports: {ports}"
188
197
  raise ValueError(
189
198
  msg
190
199
  )
191
- # Note: baud rate may not *always* work. These are a superset
200
+ # Note: baud rate may not *always* work. These are a superset.
192
201
  try:
193
202
  baud = int(baud)
194
203
  except ValueError:
File without changes
@@ -0,0 +1,19 @@
1
+ """ ccsds.apid: APID mapping functions for F´ data """
2
+ from fprime_gds.common.utils.data_desc_type import DataDescType
3
+ from fprime.common.models.serialize.numerical_types import U32Type
4
+
5
+ class APID(object):
6
+ """ APID implementations """
7
+ #TODO: use the DataDescType configured by loading the dictionary
8
+
9
+ @classmethod
10
+ def from_type(cls, data_type: DataDescType):
11
+ """ Map from data description type to APID """
12
+ return data_type.value
13
+
14
+ @classmethod
15
+ def from_data(cls, data):
16
+ """ Map from data bytes to APID """
17
+ u32_type = U32Type()
18
+ u32_type.deserialize(data, offset=0)
19
+ return cls.from_type(DataDescType(u32_type.val))
@@ -0,0 +1,106 @@
1
+ """ fprime_encryption.framing.chain: implementation of a chained framer/deframer """
2
+ from abc import ABC, abstractmethod
3
+ from functools import reduce
4
+ from typing import Any, Dict, List, Type
5
+ from fprime_gds.common.communication.framing import FramerDeframer
6
+ from fprime_gds.common.communication.ccsds.space_data_link import SpaceDataLinkFramerDeframer
7
+ from fprime_gds.common.communication.ccsds.space_packet import SpacePacketFramerDeframer
8
+ from fprime_gds.plugin.definitions import gds_plugin
9
+
10
+
11
+
12
+ class ChainedFramerDeframer(FramerDeframer, ABC):
13
+ """ Framer/deframer that is a composite of chained framer/deframers
14
+
15
+ This Framer/Deframer will wrap a set of framer/deframers where the result of the frame and deframe options will pass
16
+ from one to the other subsequently. The order is specified via the framing path and deframing will use the reverse
17
+ order from specified.
18
+ """
19
+ def __init__(self, **kwargs):
20
+ """ Initialize the chained framer/deframer from a framing-ordered set of children """
21
+ frame_order_framer_deframers = [
22
+ composite(**self.get_argument_subset(composite, kwargs))
23
+ for composite in self.get_composites()
24
+ ]
25
+ self.framers = frame_order_framer_deframers[::1]
26
+ self.deframers = frame_order_framer_deframers[::-1]
27
+
28
+ @classmethod
29
+ @abstractmethod
30
+ def get_composites(cls) -> List[Type[FramerDeframer]]:
31
+ """ Return a list of composites
32
+ Innermost FramerDeframer should be first in the list. """
33
+ raise NotImplementedError(f"Subclasses of {cls.__name__} must implement get_composites")
34
+
35
+ @staticmethod
36
+ def get_argument_subset(composite: Type[FramerDeframer], argument_dictionary: Dict[str, Any]) -> Dict[str, Any]:
37
+ """ Get an argument subset that is needed by composite
38
+
39
+ For the composite, find the set of arguments that is needed by this composite and pull those out of the complete
40
+ argument dictionary.
41
+
42
+ Args:
43
+ composite: class of a subtype of FramerDeframer
44
+ argument_dictionary: dictionary of all input arguments
45
+ """
46
+ if not hasattr(composite, "get_arguments"):
47
+ return {}
48
+ needed_arguments = composite.get_arguments()
49
+ needed_argument_destinations = [
50
+ description["destination"] if "destination" in description else
51
+ [dash_dash for dash_dash in flag if dash_dash.startswith("--")][0].lstrip("-").replace("-", "_")
52
+ for flag, description in needed_arguments.items()
53
+ ]
54
+ return {name: argument_dictionary[name] for name in needed_argument_destinations}
55
+
56
+ @classmethod
57
+ def get_arguments(cls):
58
+ """ Arguments to request from the CLI """
59
+ all_arguments = {}
60
+ for composite in cls.get_composites():
61
+ all_arguments.update(composite.get_arguments() if hasattr(composite, "get_arguments") else {})
62
+ return all_arguments
63
+
64
+ @classmethod
65
+ def check_arguments(cls, **kwargs):
66
+ """ Check arguments from the CLI """
67
+ for composite in cls.get_composites():
68
+ subset_arguments = cls.get_argument_subset(composite, kwargs)
69
+ if hasattr(composite, "check_arguments"):
70
+ composite.check_arguments(**subset_arguments)
71
+
72
+ def deframe(self, data, no_copy=False):
73
+ """ Deframe via a chain of children deframers """
74
+ packet = data[:] if not no_copy else data
75
+ remaining = None
76
+ discarded = b""
77
+
78
+ for deframer in self.deframers:
79
+ new_packet, new_remaining, new_discarded = deframer.deframe(packet, True)
80
+ discarded += new_discarded
81
+ remaining = new_remaining if remaining is None else remaining
82
+ packet = new_packet
83
+ return packet, remaining, discarded
84
+
85
+ def frame(self, data):
86
+ """ Frame via a chain of children framers """
87
+ return reduce(lambda framed_data, framer: framer.frame(framed_data), self.framers, data)
88
+
89
+
90
+ @gds_plugin(FramerDeframer)
91
+ class SpacePacketSpaceDataLinkFramerDeframer(ChainedFramerDeframer):
92
+ """ Space Data Link Protocol framing and deframing that has a data unit of Space Packets as the central """
93
+
94
+ @classmethod
95
+ def get_composites(cls) -> List[Type[FramerDeframer]]:
96
+ """ Return the composite list of this chain
97
+ Innermost FramerDeframer should be first in the list. """
98
+ return [
99
+ SpacePacketFramerDeframer,
100
+ SpaceDataLinkFramerDeframer
101
+ ]
102
+
103
+ @classmethod
104
+ def get_name(cls):
105
+ """ Name of this implementation provided to CLI """
106
+ return "space-packet-space-data-link"
@@ -0,0 +1,196 @@
1
+ """F Prime Framer/Deframer Implementation of the CCSDS Space Data Link (TC/TM) Protocols"""
2
+ import sys
3
+ import struct
4
+ import copy
5
+
6
+ from fprime_gds.common.communication.framing import FramerDeframer
7
+ from fprime_gds.plugin.definitions import gds_plugin_implementation
8
+
9
+ import crc
10
+
11
+
12
+ class SpaceDataLinkFramerDeframer(FramerDeframer):
13
+ """CCSDS Framer/Deframer Implementation for the TC (uplink / framing) and TM (downlink / deframing)
14
+ protocols. This FramerDeframer is used for framing TC data for uplink and deframing TM data for downlink.
15
+ """
16
+
17
+ SEQUENCE_NUMBER_MAXIMUM = 256
18
+ TC_HEADER_SIZE = 5
19
+ TM_HEADER_SIZE = 6
20
+ TM_FIXED_FRAME_SIZE = 1024
21
+ TM_TRAILER_SIZE = 2
22
+ TC_TRAILER_SIZE = 2
23
+
24
+ # As per CCSDS standard, use CRC-16 CCITT config with init value
25
+ # all 1s and final XOR value of 0x0000
26
+ CRC_CCITT_CONFIG = crc.Configuration(
27
+ width=16,
28
+ polynomial=0x1021,
29
+ init_value=0xFFFF,
30
+ final_xor_value=0x0000,
31
+ )
32
+ CRC_CALCULATOR = crc.Calculator(CRC_CCITT_CONFIG)
33
+
34
+ def __init__(self, scid, vcid):
35
+ """ """
36
+ self.scid = scid
37
+ self.vcid = vcid
38
+ self.sequence_number = 0
39
+
40
+ def frame(self, data):
41
+ """Frame the supplied data in a TC frame"""
42
+ space_packet_bytes = data
43
+ # CCSDS TC protocol defines the length token as number of bytes in full frame, minus 1
44
+ # so we add to packet size the size of the header and trailer and subtract 1
45
+ length = (
46
+ len(space_packet_bytes) + self.TC_HEADER_SIZE + self.TC_TRAILER_SIZE - 1
47
+ )
48
+ assert length < (pow(2, 10) - 1), "Length too-large for CCSDS format"
49
+
50
+ # CCSDS TC Header:
51
+ # 2b - 00 - TF version number
52
+ # 1b - 0/1 - 0 enable FARM checks, 1 bypass FARM
53
+ # 1b - 0/1 - 0 = data (Type-D), 1 = control information (Type-C)
54
+ # 2b - 00 - Reserved
55
+ # 10b - XX - Spacecraft id
56
+ # 6b - XX - Virtual Channel ID
57
+ # 10b - XX - Frame length
58
+ # 8b - XX - Frame sequence number
59
+
60
+ # First 16 bits:
61
+ header_val1_u16 = (
62
+ (0 << 14) | # TF version number (2 bits)
63
+ (1 << 13) | # Bypass FARM (1 bit)
64
+ (0 << 12) | # Type-D (1 bit)
65
+ (0 << 10) | # Reserved (2 bits)
66
+ ((self.scid & 0x3FF)) # SCID (10 bits)
67
+ )
68
+ # Second 16 bits:
69
+ header_val2_u16 = (
70
+ ((self.vcid & 0x3F) << 10) | # VCID (6 bits)
71
+ (length & 0x3FF) # Frame length (10 bits)
72
+ )
73
+ # 8 bit sequence number - always 0 in bypass FARM mode
74
+ header_val3_u8 = 0
75
+ header_bytes = struct.pack(">HHB", header_val1_u16, header_val2_u16, header_val3_u8)
76
+ full_bytes_no_crc = header_bytes + space_packet_bytes
77
+ assert (
78
+ len(header_bytes) == self.TC_HEADER_SIZE
79
+ ), "CCSDS primary header must be 5 octets long"
80
+ assert len(full_bytes_no_crc) == self.TC_HEADER_SIZE + len(
81
+ data
82
+ ), "Malformed packet generated"
83
+
84
+ full_bytes = full_bytes_no_crc + struct.pack(
85
+ ">H", self.CRC_CALCULATOR.checksum(full_bytes_no_crc)
86
+ )
87
+ return full_bytes
88
+
89
+ def get_sequence_number(self):
90
+ """Get the sequence number and increment - used for TM deframing
91
+
92
+ This function will return the current sequence number and then increment the sequence number for the next round.
93
+
94
+ Return:
95
+ current sequence number
96
+ """
97
+ sequence = self.sequence_number
98
+ self.sequence_number = (self.sequence_number + 1) % self.SEQUENCE_NUMBER_MAXIMUM
99
+ return sequence
100
+
101
+ def deframe(self, data, no_copy=False):
102
+ """Deframe TM frames"""
103
+ discarded = b""
104
+ if not no_copy:
105
+ data = copy.copy(data)
106
+ # Continue until there is not enough data for the header, or until a packet is found (return)
107
+ while len(data) >= self.TM_FIXED_FRAME_SIZE:
108
+ # Read header information
109
+ sc_and_channel_ids = struct.unpack_from(">H", data)
110
+ spacecraft_id = (sc_and_channel_ids[0] & 0x3FF0) >> 4
111
+ virtual_channel_id = (sc_and_channel_ids[0] & 0x000E) >> 1
112
+ # Check if the header is correct with regards to expected spacecraft and VC IDs
113
+ if spacecraft_id != self.scid or virtual_channel_id != self.vcid:
114
+ # If the header is invalid, rotate away a Byte and keep processing
115
+ discarded += data[0:1]
116
+ data = data[1:]
117
+ continue
118
+ # Spacecraft ID and Virtual Channel ID match, so we look at end of frame for CRC
119
+ crc_offset = self.TM_FIXED_FRAME_SIZE - self.TM_TRAILER_SIZE
120
+ transmitted_crc = struct.unpack_from(">H", data, crc_offset)[0]
121
+ if transmitted_crc == self.CRC_CALCULATOR.checksum(data[:crc_offset]):
122
+ # CRC is valid, so we return the deframed data
123
+ deframed_data_len = (
124
+ self.TM_FIXED_FRAME_SIZE
125
+ - self.TM_TRAILER_SIZE
126
+ - self.TM_HEADER_SIZE
127
+ )
128
+ deframed = struct.unpack_from(
129
+ f">{deframed_data_len}s", data, self.TM_HEADER_SIZE
130
+ )[0]
131
+ # Consume the fixed size frame
132
+ data = data[self.TM_FIXED_FRAME_SIZE :]
133
+ return deframed, data, discarded
134
+
135
+ print(
136
+ "[WARNING] Checksum validation failed.",
137
+ file=sys.stderr,
138
+ )
139
+ # Bad checksum, rotate 1 and keep looking for non-garbage
140
+ discarded += data[0:1]
141
+ data = data[1:]
142
+ continue
143
+ return None, data, discarded
144
+
145
+ @classmethod
146
+ def get_arguments(cls):
147
+ """Arguments to request from the CLI"""
148
+ return {
149
+ ("--scid",): {
150
+ "type": lambda input_arg: int(input_arg, 0),
151
+ "help": "Spacecraft ID",
152
+ "default": 0x44,
153
+ "required": False,
154
+ },
155
+ ("--vcid",): {
156
+ "type": lambda input_arg: int(input_arg, 0),
157
+ "help": "Virtual channel ID",
158
+ "default": 1,
159
+ "required": False,
160
+ },
161
+ }
162
+
163
+ @classmethod
164
+ def check_arguments(cls, scid, vcid):
165
+ """Check arguments from the CLI
166
+
167
+ Confirms that the input arguments are valid for this framer/deframer.
168
+
169
+ Args:
170
+ scid: spacecraft id
171
+ vcid: virtual channel id
172
+ """
173
+ if scid is None:
174
+ raise TypeError(f"Spacecraft ID not specified")
175
+ if scid < 0:
176
+ raise TypeError(f"Spacecraft ID {scid} is negative")
177
+ if scid > 0x3FF:
178
+ raise TypeError(f"Spacecraft ID {scid} is larger than {0x3FF}")
179
+
180
+ if vcid is None:
181
+ raise TypeError(f"Virtual Channel ID not specified")
182
+ if vcid < 0:
183
+ raise TypeError(f"Virtual Channel ID {vcid} is negative")
184
+ if vcid > 0x3F:
185
+ raise TypeError(f"Virtual Channel ID {vcid} is larger than {0x3FF}")
186
+
187
+ @classmethod
188
+ def get_name(cls):
189
+ """Name of this implementation provided to CLI"""
190
+ return "raw-space-data-link"
191
+
192
+ @classmethod
193
+ @gds_plugin_implementation
194
+ def register_framing_plugin(cls):
195
+ """Register the MyPlugin plugin"""
196
+ return cls
@@ -0,0 +1,130 @@
1
+ """F Prime Framer/Deframer Implementation of the CCSDS Space Packet Protocol"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import struct
6
+ import copy
7
+
8
+ from spacepackets.ccsds.spacepacket import SpacePacketHeader, PacketType, SpacePacket
9
+
10
+ from fprime_gds.common.communication.framing import FramerDeframer
11
+ from fprime_gds.plugin.definitions import gds_plugin_implementation, gds_plugin
12
+ from fprime_gds.common.utils.data_desc_type import DataDescType
13
+
14
+ from .apid import APID
15
+ import logging
16
+
17
+ LOGGER = logging.getLogger("framing")
18
+ LOGGER.setLevel(logging.DEBUG)
19
+
20
+
21
+ @gds_plugin(FramerDeframer)
22
+ class SpacePacketFramerDeframer(FramerDeframer):
23
+ """Concrete implementation of FramerDeframer supporting SpacePacket protocol
24
+
25
+ This implementation is registered as a "framing" plugin to support encryption within the GDS layer.
26
+ """
27
+
28
+ SEQUENCE_COUNT_MAXIMUM = 16384 # 2^14
29
+ HEADER_SIZE = 6
30
+ IDLE_APID = 0x7FF # max 11 bit value per protocol specification
31
+
32
+ def __init__(self):
33
+ # self.sequence_number = 0
34
+ # Map APID to sequence counts
35
+ self.apid_to_sequence_count_map = dict()
36
+ for key in DataDescType:
37
+ self.apid_to_sequence_count_map[key.value] = 0
38
+
39
+ def frame(self, data):
40
+ """Frame the supplied data in Space Packet"""
41
+ # The protocol defines length token to be number of bytes minus 1
42
+ data_length_token = len(data) - 1
43
+ apid = APID.from_data(data)
44
+ space_header = SpacePacketHeader(
45
+ packet_type=PacketType.TC,
46
+ apid=apid,
47
+ seq_count=self.get_sequence_count(apid),
48
+ data_len=data_length_token,
49
+ )
50
+ space_packet = SpacePacket(space_header, sec_header=None, user_data=data)
51
+ return space_packet.pack()
52
+
53
+ def deframe(self, data, no_copy=False):
54
+ """Deframe the supplied data according to Space Packet protocol"""
55
+ discarded = b""
56
+ if data is None:
57
+ return None, None, discarded
58
+ if not no_copy:
59
+ data = copy.copy(data)
60
+ # Deframe all packets until there is not enough data for a header
61
+ while len(data) >= self.HEADER_SIZE:
62
+ # Read header information including start token and size and check if we have enough for the total size
63
+ try:
64
+ sp_header = SpacePacketHeader.unpack(data)
65
+ except ValueError:
66
+ # If the header is invalid, rotate away a byte and keep processing
67
+ discarded += data[0:1]
68
+ data = data[1:]
69
+ continue
70
+ if sp_header.ccsds_version != 0 or sp_header.packet_type != PacketType.TM:
71
+ # Space Packet version is specified as 0 per protocol
72
+ discarded += data[0:1]
73
+ data = data[1:]
74
+ continue
75
+ # Skip Idle Packets as they are not meaningful
76
+ if sp_header.apid == self.IDLE_APID:
77
+ data = data[sp_header.packet_len :]
78
+ continue
79
+ # Check sequence count and warn if not expected value (don't drop the packet)
80
+ if sp_header.seq_count != self.get_sequence_count(sp_header.apid):
81
+ LOGGER.warning(
82
+ f"APID {sp_header.apid} received sequence count: {sp_header.seq_count}"
83
+ f" (expected: {self.get_sequence_count(sp_header.apid)})"
84
+ )
85
+ # Set the sequence count to the next expected value (consider missing packets have been lost)
86
+ self.apid_to_sequence_count_map[sp_header.apid] = (
87
+ sp_header.seq_count + 1
88
+ )
89
+ # If the pool is large enough to read the whole packet, then read it
90
+ if len(data) >= sp_header.packet_len:
91
+ deframed = struct.unpack_from(
92
+ # data_len is number of bytes minus 1 per SpacePacket spec
93
+ f">{sp_header.data_len + 1}s",
94
+ data,
95
+ self.HEADER_SIZE,
96
+ )[0]
97
+ data = data[sp_header.packet_len :]
98
+ LOGGER.debug(f"Deframed packet: {sp_header}")
99
+ return deframed, data, discarded
100
+ else:
101
+ # If we don't have enough data, then break out of the loop
102
+ break
103
+ return None, data, discarded
104
+
105
+ def get_sequence_count(self, apid: int):
106
+ """Get the sequence number and increment
107
+
108
+ This function will return the current sequence number and then increment the sequence number for the next round.
109
+ Should an APID not be registered already, it will be initialized to 0.
110
+
111
+ Return:
112
+ current sequence number
113
+ """
114
+ # If APID is not registered, initialize it to 0
115
+ sequence = self.apid_to_sequence_count_map.get(apid, 0)
116
+ self.apid_to_sequence_count_map[apid] = (
117
+ sequence + 1
118
+ ) % self.SEQUENCE_COUNT_MAXIMUM
119
+ return sequence
120
+
121
+ @classmethod
122
+ def get_name(cls):
123
+ """Name of this implementation provided to CLI"""
124
+ return "raw-space-packet"
125
+
126
+ @classmethod
127
+ @gds_plugin_implementation
128
+ def register_framing_plugin(cls):
129
+ """Register the MyPlugin plugin"""
130
+ return cls
@@ -11,6 +11,8 @@ that implement this pattern. The current list of implementation classes are:
11
11
 
12
12
  @author lestarch
13
13
  """
14
+
15
+ from __future__ import annotations
14
16
  import abc
15
17
  import copy
16
18
  import struct
@@ -18,7 +20,10 @@ import sys
18
20
  from typing import Type
19
21
 
20
22
  from .checksum import calculate_checksum, CHECKSUM_MAPPING
21
- from fprime_gds.plugin.definitions import gds_plugin_implementation, gds_plugin_specification
23
+ from fprime_gds.plugin.definitions import (
24
+ gds_plugin_implementation,
25
+ gds_plugin_specification,
26
+ )
22
27
 
23
28
 
24
29
  class FramerDeframer(abc.ABC):
@@ -28,7 +33,7 @@ class FramerDeframer(abc.ABC):
28
33
  """
29
34
 
30
35
  @abc.abstractmethod
31
- def frame(self, data):
36
+ def frame(self, data: bytes) -> bytes:
32
37
  """
33
38
  Frames outgoing data in the specified format. Expects incoming raw bytes to frame, and adds on the needed header
34
39
  and footer bytes. This new array of bytes is returned from the method.
@@ -38,20 +43,27 @@ class FramerDeframer(abc.ABC):
38
43
  """
39
44
 
40
45
  @abc.abstractmethod
41
- def deframe(self, data, no_copy=False):
46
+ def deframe(
47
+ self, data: bytes, no_copy=False
48
+ ) -> tuple[(bytes | None), bytes, bytes]:
42
49
  """
43
- Deframes the incoming data from the specified format. Produces exactly one packet, and leftover bytes. Users
44
- wanting all packets to be deframed should call "deframe_all". If no full packet is available, this method
50
+ Deframes the incoming data from the specified format.
51
+ Produces:
52
+ - One packet, or None if no packet found
53
+ - leftover bytes (not consumed yet)
54
+ - Discarded data (consumed and determined not to be valid)
55
+
56
+ Users wanting all packets to be deframed should call "deframe_all". If no full packet is available, this method
45
57
  returns None. Expects incoming raw bytes to deframe, and returns a deframed packet or None, the leftover
46
58
  bytes that were unused, and any bytes discarded from the existing data stream. Will search and discard data up
47
59
  until a start token is found. Note: data will be consumed up to the first start token found.
48
60
 
49
61
  :param data: framed data bytes
50
62
  :param no_copy: (optional) will prevent extra copy if True, but "data" input will be destroyed.
51
- :return: (packet as array of bytes or None, leftover bytes, any discarded data)
63
+ :return: (packet as bytes or None, leftover bytes, any discarded data)
52
64
  """
53
65
 
54
- def deframe_all(self, data, no_copy):
66
+ def deframe_all(self, data: bytes, no_copy: bool):
55
67
  """
56
68
  Deframes all available packets found in a single set of bytes by calling deframe until a None packet is
57
69
  retrieved. This list of packets, and the remaining bytes are returned
@@ -66,11 +78,11 @@ class FramerDeframer(abc.ABC):
66
78
  discarded_aggregate = b""
67
79
  while True:
68
80
  # Deframe and return only on None
69
- (packet, data, discarded) = self.deframe(data, no_copy=True)
81
+ (deframed, data, discarded) = self.deframe(data, no_copy=True)
70
82
  discarded_aggregate += discarded
71
- if packet is None:
83
+ if deframed is None: # No more packets available, return aggregate
72
84
  return packets, data, discarded_aggregate
73
- packets.append(packet)
85
+ packets.append(deframed)
74
86
 
75
87
  @classmethod
76
88
  @gds_plugin_specification
@@ -117,7 +129,7 @@ class FpFramerDeframer(FramerDeframer):
117
129
  HEADER_FORMAT = None
118
130
  START_TOKEN = None
119
131
 
120
- def __init__(self, checksum_type):
132
+ def __init__(self, checksum_type="crc32"):
121
133
  """Sets constants on construction."""
122
134
  # Setup the constants as soon as possible.
123
135
  FpFramerDeframer.set_constants()
@@ -197,13 +209,12 @@ class FpFramerDeframer(FramerDeframer):
197
209
  )
198
210
  # If the checksum is valid, return the packet. Otherwise continue to rotate
199
211
  if check == calculate_checksum(
200
- data[: data_size + FpFramerDeframer.HEADER_SIZE],
201
- self.checksum
212
+ data[: data_size + FpFramerDeframer.HEADER_SIZE], self.checksum
202
213
  ):
203
214
  data = data[total_size:]
204
215
  return deframed, data, discarded
205
216
  print(
206
- "[WARNING] Checksum validation failed. Have you correctly set '--comm-checksum-type'",
217
+ "[WARNING] Checksum validation failed.",
207
218
  file=sys.stderr,
208
219
  )
209
220
  # Bad checksum, rotate 1 and keep looking for non-garbage
@@ -216,29 +227,13 @@ class FpFramerDeframer(FramerDeframer):
216
227
 
217
228
  @classmethod
218
229
  def get_name(cls):
219
- """ Get the name of this plugin """
230
+ """Get the name of this plugin"""
220
231
  return "fprime"
221
232
 
222
- @classmethod
223
- def get_arguments(cls):
224
- """ Get arguments for the framer/deframer """
225
- return {("--comm-checksum-type",): {
226
- "dest": "checksum_type",
227
- "action": "store",
228
- "type": str,
229
- "help": "Setup the checksum algorithm. [default: %(default)s]",
230
- "choices": [
231
- item
232
- for item in CHECKSUM_MAPPING.keys()
233
- if item != "default"
234
- ],
235
- "default": "crc32",
236
- }}
237
-
238
233
  @classmethod
239
234
  @gds_plugin_implementation
240
235
  def register_framing_plugin(cls):
241
- """ Register a bad plugin """
236
+ """Register a bad plugin"""
242
237
  return cls
243
238
 
244
239
 
@@ -5,10 +5,13 @@ Defines an enumeration that represents each type of data packet that can be down
5
5
  """
6
6
  from enum import Enum
7
7
 
8
+
9
+ # TODO: these values should be read from the dictionary instead of hardcoded here
10
+
8
11
  DataDescType = Enum(
9
12
  "DataDescType",
10
- # Command packet type - incoming
11
13
  {
14
+ # Command packet type - incoming
12
15
  "FW_PACKET_COMMAND": 0,
13
16
  # Telemetry packet type - outgoing
14
17
  "FW_PACKET_TELEM": 1,
@@ -24,5 +27,7 @@ DataDescType = Enum(
24
27
  "FW_PACKET_HAND": 0xFE,
25
28
  # Unknown packet
26
29
  "FW_PACKET_UNKNOWN": 0xFF,
30
+ # Space Packet Idle APID
31
+ "CCSDS_SPACE_PACKET_IDLE_APID": 0x7FF,
27
32
  },
28
33
  )
@@ -19,6 +19,9 @@ import os
19
19
  import platform
20
20
  import re
21
21
  import sys
22
+
23
+ import yaml
24
+
22
25
  from abc import ABC, abstractmethod
23
26
  from pathlib import Path
24
27
  from typing import Any, Dict, List, Tuple
@@ -185,8 +188,32 @@ class ParserBase(ABC):
185
188
  Returns: namespace with processed results of arguments.
186
189
  """
187
190
 
188
- @staticmethod
191
+ @classmethod
192
+ def parse_known_args(
193
+ cls,
194
+ parser_classes,
195
+ description="No tool description provided",
196
+ arguments=None,
197
+ **kwargs,
198
+ ):
199
+ """Parse and post-process arguments
200
+
201
+ Create a parser for the given application using the description provided. This will then add all specified
202
+ ParserBase subclasses' get_parser output as parent parses for the created parser. Then all of the handle
203
+ arguments methods will be called, and the final namespace will be returned. This will allow unknown arguments
204
+ which are returned as the last tuple result.
205
+
206
+ Args:
207
+ parser_classes: a list of ParserBase subclasses that will be used to
208
+ description: description passed ot the argument parser
209
+ arguments: arguments to process, None to use command line input
210
+ Returns: namespace with all parsed arguments from all provided ParserBase subclasses
211
+ """
212
+ return cls._parse_args(parser_classes, description, arguments, use_parse_known=True, **kwargs)
213
+
214
+ @classmethod
189
215
  def parse_args(
216
+ cls,
190
217
  parser_classes,
191
218
  description="No tool description provided",
192
219
  arguments=None,
@@ -194,20 +221,53 @@ class ParserBase(ABC):
194
221
  ):
195
222
  """Parse and post-process arguments
196
223
 
224
+ Create a parser for the given application using the description provided. This will then add all specified
225
+ ParserBase subclasses' get_parser output as parent parses for the created parser. Then all of the handle
226
+ arguments methods will be called, and the final namespace will be returned. This does not allow unknown
227
+ arguments.
228
+
229
+ Args:
230
+ parser_classes: a list of ParserBase subclasses that will be used to
231
+ description: description passed ot the argument parser
232
+ arguments: arguments to process, None to use command line input
233
+ Returns: namespace with all parsed arguments from all provided ParserBase subclasses
234
+ """
235
+ return cls._parse_args(parser_classes, description, arguments, **kwargs)
236
+
237
+
238
+ @staticmethod
239
+ def _parse_args(
240
+ parser_classes,
241
+ description="No tool description provided",
242
+ arguments=None,
243
+ use_parse_known=False,
244
+ **kwargs,
245
+ ):
246
+ """Parse and post-process arguments helper
247
+
197
248
  Create a parser for the given application using the description provided. This will then add all specified
198
249
  ParserBase subclasses' get_parser output as parent parses for the created parser. Then all of the handle
199
250
  arguments methods will be called, and the final namespace will be returned.
200
251
 
252
+ This takes a function that will take in a parser and return the parsing function to call on arguments.
253
+
201
254
  Args:
255
+ parse_function_processor: takes a parser, returns the parse function to call
202
256
  parser_classes: a list of ParserBase subclasses that will be used to
203
257
  description: description passed ot the argument parser
204
258
  arguments: arguments to process, None to use command line input
259
+ use_parse_known: use parse_known_arguments from argparse
260
+
205
261
  Returns: namespace with all parsed arguments from all provided ParserBase subclasses
206
262
  """
207
263
  composition = CompositeParser(parser_classes, description)
208
264
  parser = composition.get_parser()
209
265
  try:
210
- args_ns = parser.parse_args(arguments)
266
+ if use_parse_known:
267
+ args_ns, *unknowns = parser.parse_known_args(arguments)
268
+ else:
269
+ args_ns = parser.parse_args(arguments)
270
+ unknowns = []
211
271
  args_ns = composition.handle_arguments(args_ns, **kwargs)
212
272
  except ValueError as ver:
213
273
  print(f"[ERROR] Failed to parse arguments: {ver}", file=sys.stderr)
@@ -216,7 +276,7 @@ class ParserBase(ABC):
216
276
  except Exception as exc:
217
277
  print(f"[ERROR] {exc}", file=sys.stderr)
218
278
  sys.exit(-1)
219
- return args_ns, parser
279
+ return args_ns, parser, *unknowns
220
280
 
221
281
  @staticmethod
222
282
  def find_in(token, deploy, is_file=True):
@@ -236,6 +296,106 @@ class ParserBase(ABC):
236
296
  return None
237
297
 
238
298
 
299
+ class ConfigDrivenParser(ParserBase):
300
+ """ Parser that allows options from configuration and command line
301
+
302
+ This parser reads a configuration file (if supplied) and uses the values to drive the inputs to arguments. Command
303
+ line arguments will still take precedence over the configured values.
304
+ """
305
+ DEFAULT_CONFIGURATION_PATH = Path("fprime-gds.yml")
306
+
307
+ @classmethod
308
+ def set_default_configuration(cls, path: Path):
309
+ """ Set path for (global) default configuration file
310
+
311
+ Set the path for default configuration file. If unset, will use 'fprime-gds.yml'. Set to None to disable default
312
+ configuration.
313
+ """
314
+ cls.DEFAULT_CONFIGURATION_PATH = path
315
+
316
+ @classmethod
317
+ def parse_args(
318
+ cls,
319
+ parser_classes,
320
+ description="No tool description provided",
321
+ arguments=None,
322
+ **kwargs,
323
+ ):
324
+ """ Parse and post-process arguments using inputs and config
325
+
326
+ Parse the arguments in two stages: first parse the configuration data, ignoring unknown inputs, then parse the
327
+ full argument set with the supplied configuration to fill in additional options.
328
+
329
+ Args:
330
+ parser_classes: a list of ParserBase subclasses that will be used to
331
+ description: description passed ot the argument parser
332
+ arguments: arguments to process, None to use command line input
333
+ Returns: namespace with all parsed arguments from all provided ParserBase subclasses
334
+ """
335
+ arguments = sys.argv[1:] if arguments is None else arguments
336
+
337
+ # Help should spill all the arguments, so delegate to the normal parsing flow including
338
+ # this and supplied parsers
339
+ if "-h" in arguments or "--help" in arguments:
340
+ parsers = [ConfigDrivenParser] + parser_classes
341
+ ParserBase.parse_args(parsers, description, arguments, **kwargs)
342
+ sys.exit(0)
343
+
344
+ # Custom flow involving parsing the arguments of this parser first, then passing the configured values
345
+ # as part of the argument source
346
+ ns_config, _, remaining = ParserBase.parse_known_args([ConfigDrivenParser], description, arguments, **kwargs)
347
+ config_options = ns_config.config_values.get("command-line-options", {})
348
+ config_args = cls.flatten_options(config_options)
349
+ # Argparse allows repeated (overridden) arguments, thus the CLI override is accomplished by providing
350
+ # remaining arguments after the configured ones
351
+ ns_full, parser = ParserBase.parse_args(parser_classes, description, config_args + remaining, **kwargs)
352
+ ns_final = argparse.Namespace(**vars(ns_config), **vars(ns_full))
353
+ return ns_final, parser
354
+
355
+ @staticmethod
356
+ def flatten_options(configured_options):
357
+ """ Flatten options down to arguments """
358
+ flattened = []
359
+ for option, value in configured_options.items():
360
+ flattened.append(f"--{option}")
361
+ if value is not None:
362
+ flattened.extend(value if isinstance(value, (list, tuple)) else [f"{value}"])
363
+ return flattened
364
+
365
+ def get_arguments(self) -> Dict[Tuple[str, ...], Dict[str, Any]]:
366
+ """Arguments needed for config processing"""
367
+ return {
368
+ ("-c", "--config"): {
369
+ "dest": "config",
370
+ "required": False,
371
+ "default": self.DEFAULT_CONFIGURATION_PATH,
372
+ "type": Path,
373
+ "help": f"Argument configuration file path.",
374
+ }
375
+ }
376
+
377
+ def handle_arguments(self, args, **kwargs):
378
+ """ Handle the arguments
379
+
380
+ Loads the configuration file specified and fills in the `config_values` attribute of the namespace with the
381
+ loaded configuration dictionary.
382
+ """
383
+ args.config_values = {}
384
+ # Specified but non-existent config file is a hard error
385
+ if ("-c" in sys.argv[1:] or "--config" in sys.argv[1:]) and not args.config.exists():
386
+ raise ValueError(f"Specified configuration file '{args.config}' does not exist")
387
+ # Read configuration if the file was set and exists
388
+ if args.config is not None and args.config.exists():
389
+ print(f"[INFO] Reading command-line configuration from: {args.config}")
390
+ with open(args.config, "r") as file_handle:
391
+ try:
392
+ loaded = yaml.safe_load(file_handle)
393
+ args.config_values = loaded if loaded is not None else {}
394
+ except Exception as exc:
395
+ raise ValueError(f"Malformed configuration {args.config}: {exc}", exc)
396
+ return args
397
+
398
+
239
399
  class DetectionParser(ParserBase):
240
400
  """Parser that detects items from a root/directory or deployment"""
241
401
 
@@ -398,6 +558,7 @@ class IndividualPluginParser(BareArgumentParser):
398
558
  for key, value in self.extract_arguments(arguments).items()
399
559
  if key != self.disable_flag_destination
400
560
  }
561
+
401
562
  plugin_zero_argument_class = functools.partial(
402
563
  self.plugin_class.get_implementor(), **plugin_arguments
403
564
  )
@@ -9,6 +9,7 @@ import webbrowser
9
9
 
10
10
  from fprime_gds.executables.cli import (
11
11
  BinaryDeployment,
12
+ ConfigDrivenParser,
12
13
  CommParser,
13
14
  GdsParser,
14
15
  ParserBase,
@@ -37,7 +38,7 @@ def parse_args():
37
38
  PluginArgumentParser,
38
39
  ]
39
40
  # Parse the arguments, and refine through all handlers
40
- args, parser = ParserBase.parse_args(arg_handlers, "Run F prime deployment and GDS")
41
+ args, parser = ConfigDrivenParser.parse_args(arg_handlers, "Run F prime deployment and GDS")
41
42
  return args
42
43
 
43
44
 
@@ -218,6 +218,7 @@ class Plugins(object):
218
218
  FramerDeframer,
219
219
  FpFramerDeframer,
220
220
  )
221
+ from fprime_gds.common.communication.ccsds.chain import SpacePacketSpaceDataLinkFramerDeframer
221
222
  from fprime_gds.common.communication.adapters.base import (
222
223
  BaseAdapter,
223
224
  NoneAdapter,
@@ -233,7 +234,7 @@ class Plugins(object):
233
234
  "framing": {
234
235
  "class": FramerDeframer,
235
236
  "type": PluginType.SELECTION,
236
- "built-in": [FpFramerDeframer],
237
+ "built-in": [FpFramerDeframer, SpacePacketSpaceDataLinkFramerDeframer],
237
238
  },
238
239
  "communication": {
239
240
  "class": BaseAdapter,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fprime-gds
3
- Version: 4.0.0a1
3
+ Version: 4.0.0a3
4
4
  Summary: F Prime Flight Software Ground Data System layer
5
5
  Author-email: Michael Starch <Michael.D.Starch@jpl.nasa.gov>, Thomas Boyer-Chammard <Thomas.Boyer.Chammard@jpl.nasa.gov>
6
6
  License:
@@ -216,15 +216,15 @@ Classifier: Operating System :: Unix
216
216
  Classifier: Operating System :: POSIX
217
217
  Classifier: Programming Language :: Python
218
218
  Classifier: Programming Language :: Python :: 3
219
- Classifier: Programming Language :: Python :: 3.8
220
219
  Classifier: Programming Language :: Python :: 3.9
221
220
  Classifier: Programming Language :: Python :: 3.10
222
221
  Classifier: Programming Language :: Python :: 3.11
223
222
  Classifier: Programming Language :: Python :: 3.12
223
+ Classifier: Programming Language :: Python :: 3.13
224
224
  Classifier: Programming Language :: Python :: Implementation :: CPython
225
225
  Classifier: Programming Language :: Python :: Implementation :: PyPy
226
226
  Classifier: License :: OSI Approved :: Apache Software License
227
- Requires-Python: >=3.8
227
+ Requires-Python: >=3.9
228
228
  Description-Content-Type: text/markdown
229
229
  License-File: LICENSE.txt
230
230
  License-File: NOTICE.txt
@@ -240,6 +240,9 @@ Requires-Dist: Jinja2>=2.11.3
240
240
  Requires-Dist: openpyxl>=3.0.10
241
241
  Requires-Dist: pyserial>=3.5
242
242
  Requires-Dist: pydantic>=2.6
243
+ Requires-Dist: PyYAML>=6.0.2
244
+ Requires-Dist: spacepackets>=0.28.0
245
+ Requires-Dist: crc>=7.0.0
243
246
  Dynamic: license-file
244
247
 
245
248
  # F´ GDS
@@ -7,13 +7,18 @@ fprime_gds/common/transport.py,sha256=uYXWkM8TYEYz1vfY4AEn0PF8Gu4tkYmJ5t4w1YY1yW
7
7
  fprime_gds/common/zmq_transport.py,sha256=E_iBZ5sA4JKB99MWSOM6XnPrO-mbFyRvD9eQp9te6-Y,12397
8
8
  fprime_gds/common/communication/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  fprime_gds/common/communication/checksum.py,sha256=f6W0Tr68U-XGnFmysMqsFzoGYZVE8clKf-VIJja_1YM,741
10
- fprime_gds/common/communication/framing.py,sha256=TPpVn5JfGJxdc9BuKJzm5LXo6OQKtkSqX695UdN-Ezk,12915
10
+ fprime_gds/common/communication/framing.py,sha256=GnEUgHiTc0dEJ3coaVmcA03NXk8ehHfPaywB7hNnoO8,12658
11
11
  fprime_gds/common/communication/ground.py,sha256=9SD3AoyHA43yNE8UYkWnu5nEJt1PgyB3sU3QLDc4eDY,3619
12
12
  fprime_gds/common/communication/updown.py,sha256=UhfCIIA2eM5g2FsIhOGJJH6HzHurUPgcKIJ5fsLb2lE,9888
13
13
  fprime_gds/common/communication/adapters/__init__.py,sha256=ivGtzUTqhBYuve5mhN9VOHITwgZjNMVv7sxuac2Ll3c,470
14
14
  fprime_gds/common/communication/adapters/base.py,sha256=i3mf4HC-4tuf4mNkhdXCKlngRhODyTriia2pw6XBoSQ,3393
15
15
  fprime_gds/common/communication/adapters/ip.py,sha256=vCDclpsb3rVRXSxKqdt9UfkM2M6oCxnsKdzbzhMc0kM,17074
16
- fprime_gds/common/communication/adapters/uart.py,sha256=6SrN42ShVjwNubFg-1YrO09o1uJtu1rFqeMpLDNWlW4,6647
16
+ fprime_gds/common/communication/adapters/uart.py,sha256=5WkA8xpQ8E7nv2DbN168fibz1l-GddJUKnf6Hcd4hvU,7194
17
+ fprime_gds/common/communication/ccsds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ fprime_gds/common/communication/ccsds/apid.py,sha256=Y5K_xHLo1bmpxOlkt-TSLulCXbKIQrbYfa3GXhadqEE,686
19
+ fprime_gds/common/communication/ccsds/chain.py,sha256=Rkhls55BUwFU0cMlRMY183hlFpfqidQJ9ZUE1kdfi38,4637
20
+ fprime_gds/common/communication/ccsds/space_data_link.py,sha256=pDi1JpmYBuKGsDgTX80Wp8PU_CDtDPtkzdnX1FXN5eM,7385
21
+ fprime_gds/common/communication/ccsds/space_packet.py,sha256=JAPpefcNya_XHGOze9WtYnYCL0NjweSLSfdX1eZEVjg,5221
17
22
  fprime_gds/common/controllers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
23
  fprime_gds/common/data_types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
24
  fprime_gds/common/data_types/ch_data.py,sha256=RP9zSyzNcH0nJ3MYyW_IATnmnHYZ6d0KmoJUJantdBI,6111
@@ -107,16 +112,16 @@ fprime_gds/common/tools/params.py,sha256=htnMLlUW9HmBo4Qc7kYhnWr1sO6bK2mckdskLt5
107
112
  fprime_gds/common/tools/seqgen.py,sha256=O57igktjWku5OJhBqezhCjPYUmh4GZM-9qKCChqEW7g,6034
108
113
  fprime_gds/common/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
109
114
  fprime_gds/common/utils/config_manager.py,sha256=EurtNdApA9zpIjAXmGSTgFUen0UaVDryH5g9LwMhu1E,5539
110
- fprime_gds/common/utils/data_desc_type.py,sha256=9GV8hV5q1dDxdfF-1-Wty5MBrFd94EbZ8hpHHkBJKuo,715
115
+ fprime_gds/common/utils/data_desc_type.py,sha256=0AkEMfEa5refd_moovf1hkgKiNakADRzv4soghvf9a4,883
111
116
  fprime_gds/common/utils/event_severity.py,sha256=7qPXHrDaM_REJ7sKBUEJTZIE0D4qVnVajsPDUuHg7sI,300
112
117
  fprime_gds/common/utils/string_util.py,sha256=u_2iahRG3ROu3lAAt_KVcK226gEByElXqrA8mH8eDpI,3584
113
118
  fprime_gds/executables/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
114
119
  fprime_gds/executables/apps.py,sha256=iyHloLaHJOkcZF6O3oXZX6YpoS2SBBZVPilf25l0Q98,13138
115
- fprime_gds/executables/cli.py,sha256=kdwQ3Wqi7zg0v3QkOyy3YEh3w3T520yGmIMJEk4mAzs,43180
120
+ fprime_gds/executables/cli.py,sha256=aNO8saIKuTDQMwWNFlrpIL5HuwIWWei94Wxf3G0lY3k,50382
116
121
  fprime_gds/executables/comm.py,sha256=08rO0o0MJgTRngB7Ygu2IL_gEAWKF7WFvFyro1CqReE,5214
117
122
  fprime_gds/executables/data_product_writer.py,sha256=aXnQ75hQ8bapz-sr21mrPCrXIfqQblfBuB49GGZrFLg,34965
118
123
  fprime_gds/executables/fprime_cli.py,sha256=CMoT7zWNwM8h2mSZW03AR96wl_XnZXoLNiOZN_sDi38,12431
119
- fprime_gds/executables/run_deployment.py,sha256=sOtvVcNOSUZMvELc4cDVAXhBNAFIWM3OjxLq_sXf0tc,7177
124
+ fprime_gds/executables/run_deployment.py,sha256=x6auxjsDyCj4216JbO0bSskv2H9r7n3vuu5Z3O5cGwY,7209
120
125
  fprime_gds/executables/tcpserver.py,sha256=KspVpu5YIuiWKOk5E6UDMKvqXYrRB1j9aX8CkMxysfw,17555
121
126
  fprime_gds/executables/utils.py,sha256=SbzXRe1p41qMPdifvPap5_4v0T42gZZ_Rs_OYfITd80,7626
122
127
  fprime_gds/flask/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -232,11 +237,11 @@ fprime_gds/flask/static/third-party/webfonts/fa-solid-900.woff,sha256=P200iM9lN0
232
237
  fprime_gds/flask/static/third-party/webfonts/fa-solid-900.woff2,sha256=mDS4KtJuKjdYPSJnahLdLrD-fIA1aiEU0NsaqLOJlTc,78268
233
238
  fprime_gds/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
234
239
  fprime_gds/plugin/definitions.py,sha256=QlxW1gNvoiqGMslSJjh3dTFZuv0igFHawN__3XJ0Wns,5355
235
- fprime_gds/plugin/system.py,sha256=UiNrUqfi-KJOgRIOR8uyFMKdquSPZh_txpNq8eH35-Y,13285
236
- fprime_gds-4.0.0a1.dist-info/licenses/LICENSE.txt,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
237
- fprime_gds-4.0.0a1.dist-info/licenses/NOTICE.txt,sha256=vXjA_xRcQhd83Vfk5D_vXg5kOjnnXvLuMi5vFKDEVmg,1612
238
- fprime_gds-4.0.0a1.dist-info/METADATA,sha256=BLViE79kzUK1-kjXnrjPrsQ1Syaugyyo4eWgL9EiqVM,24457
239
- fprime_gds-4.0.0a1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
240
- fprime_gds-4.0.0a1.dist-info/entry_points.txt,sha256=qFBHIR7CZ5CEeSEdZ-ZVQN9ZfUOZfm0PvvDZAAheuLk,445
241
- fprime_gds-4.0.0a1.dist-info/top_level.txt,sha256=6vzFLIX6ANfavKaXFHDMSLFtS94a6FaAsIWhjgYuSNE,27
242
- fprime_gds-4.0.0a1.dist-info/RECORD,,
240
+ fprime_gds/plugin/system.py,sha256=M9xb-8jBhCUUx3X1z2uAP8Wx_v6NkL8JeaFgGcMnQqY,13432
241
+ fprime_gds-4.0.0a3.dist-info/licenses/LICENSE.txt,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
242
+ fprime_gds-4.0.0a3.dist-info/licenses/NOTICE.txt,sha256=vXjA_xRcQhd83Vfk5D_vXg5kOjnnXvLuMi5vFKDEVmg,1612
243
+ fprime_gds-4.0.0a3.dist-info/METADATA,sha256=Qs6683dylX3zRC8sL3vzh6yLDwtok_LhG6xPVocO1q4,24549
244
+ fprime_gds-4.0.0a3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
245
+ fprime_gds-4.0.0a3.dist-info/entry_points.txt,sha256=qFBHIR7CZ5CEeSEdZ-ZVQN9ZfUOZfm0PvvDZAAheuLk,445
246
+ fprime_gds-4.0.0a3.dist-info/top_level.txt,sha256=6vzFLIX6ANfavKaXFHDMSLFtS94a6FaAsIWhjgYuSNE,27
247
+ fprime_gds-4.0.0a3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.7.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5