fprime-gds 3.4.3__py3-none-any.whl → 3.4.4a2__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.
Files changed (37) hide show
  1. fprime_gds/common/communication/adapters/base.py +30 -58
  2. fprime_gds/common/communication/adapters/ip.py +23 -5
  3. fprime_gds/common/communication/adapters/uart.py +20 -7
  4. fprime_gds/common/communication/checksum.py +1 -3
  5. fprime_gds/common/communication/framing.py +53 -4
  6. fprime_gds/common/data_types/event_data.py +6 -1
  7. fprime_gds/common/data_types/exceptions.py +16 -11
  8. fprime_gds/common/loaders/ch_json_loader.py +107 -0
  9. fprime_gds/common/loaders/ch_xml_loader.py +5 -5
  10. fprime_gds/common/loaders/cmd_json_loader.py +85 -0
  11. fprime_gds/common/loaders/dict_loader.py +1 -1
  12. fprime_gds/common/loaders/event_json_loader.py +108 -0
  13. fprime_gds/common/loaders/event_xml_loader.py +10 -6
  14. fprime_gds/common/loaders/json_loader.py +222 -0
  15. fprime_gds/common/loaders/xml_loader.py +31 -9
  16. fprime_gds/common/pipeline/dictionaries.py +38 -3
  17. fprime_gds/common/tools/seqgen.py +4 -4
  18. fprime_gds/common/utils/string_util.py +57 -65
  19. fprime_gds/common/zmq_transport.py +37 -20
  20. fprime_gds/executables/apps.py +150 -0
  21. fprime_gds/executables/cli.py +239 -103
  22. fprime_gds/executables/comm.py +17 -27
  23. fprime_gds/executables/data_product_writer.py +935 -0
  24. fprime_gds/executables/run_deployment.py +55 -14
  25. fprime_gds/executables/utils.py +24 -12
  26. fprime_gds/flask/sequence.py +1 -1
  27. fprime_gds/flask/static/addons/commanding/command-input.js +3 -2
  28. fprime_gds/plugin/__init__.py +0 -0
  29. fprime_gds/plugin/definitions.py +71 -0
  30. fprime_gds/plugin/system.py +225 -0
  31. {fprime_gds-3.4.3.dist-info → fprime_gds-3.4.4a2.dist-info}/METADATA +3 -2
  32. {fprime_gds-3.4.3.dist-info → fprime_gds-3.4.4a2.dist-info}/RECORD +37 -28
  33. {fprime_gds-3.4.3.dist-info → fprime_gds-3.4.4a2.dist-info}/WHEEL +1 -1
  34. {fprime_gds-3.4.3.dist-info → fprime_gds-3.4.4a2.dist-info}/entry_points.txt +2 -3
  35. {fprime_gds-3.4.3.dist-info → fprime_gds-3.4.4a2.dist-info}/LICENSE.txt +0 -0
  36. {fprime_gds-3.4.3.dist-info → fprime_gds-3.4.4a2.dist-info}/NOTICE.txt +0 -0
  37. {fprime_gds-3.4.3.dist-info → fprime_gds-3.4.4a2.dist-info}/top_level.txt +0 -0
@@ -9,12 +9,51 @@ Note: This function has an identical copy in fprime-gds
9
9
 
10
10
  import logging
11
11
  import re
12
+ from typing import Any, Union
12
13
 
13
14
  LOGGER = logging.getLogger("string_util_logger")
14
15
 
15
16
 
16
- def format_string_template(format_str, given_values):
17
- r"""
17
+ def format_string_template(template: str, value: Union[tuple, list, Any]) -> str:
18
+ """
19
+ Function to format a string template with values. This function is a simple wrapper around the
20
+ format function. It accepts a tuple, list, or single value and passes it to the format function
21
+
22
+ Args:
23
+ template (str): String template to be formatted
24
+ value (Union[tuple, list, Any]): Value(s) to be inserted into the template
25
+
26
+ Returns:
27
+ str: Formatted string
28
+ """
29
+ if not isinstance(value, (tuple, list)):
30
+ value = (value,)
31
+ try:
32
+ return template.format(*value)
33
+ except (IndexError, ValueError) as e:
34
+ LOGGER.error(
35
+ f"Error formatting string template: {template} with value: {str(value)}"
36
+ )
37
+ raise e
38
+
39
+
40
+ def preprocess_fpp_format_str(format_str: str) -> str:
41
+ """Preprocess a FPP-style format string and convert it to Python format string
42
+ FPP format strings are documented https://nasa.github.io/fpp/fpp-spec.html#Format-Strings
43
+ For example "{x}" -> "{:x}" or "{.2f}" -> "{:.2f}"
44
+
45
+ Args:
46
+ format_str (str): FPP-style format string
47
+
48
+ Returns:
49
+ str: Python-style format string
50
+ """
51
+ pattern = r"{(\d*\.?\d*[cdxoefgCDXOEFG])}"
52
+ return re.sub(pattern, r"{:\1}", format_str)
53
+
54
+
55
+ def preprocess_c_style_format_str(format_str: str) -> str:
56
+ """
18
57
  Function to convert C-string style to python format
19
58
  without using python interpolation
20
59
  Considered the following format for C-string:
@@ -31,20 +70,26 @@ def format_string_template(format_str, given_values):
31
70
  This function will keep the flags, width, and .precision of C-string
32
71
  template.
33
72
 
34
- It will keep f, d, x, o, and e flags and remove all other types.
35
- Other types will be duck-typed by python
36
- interpreter.
73
+ It will keep f, x, o, and e flags and remove all other types.
74
+ Other types will be duck-typed by python interpreter.
37
75
 
38
76
  lengths will also be removed since they are not meaningful to Python interpreter.
39
77
  `See: https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting`
40
-
41
78
  `Regex Source: https://www.regexlib.com/REDetails.aspx?regexp_id=3363`
79
+
80
+ For example "%x" -> "{:x}" or "%.2f" -> "{:.2f}"
81
+
82
+ Args:
83
+ format_str (str): C-style format string
84
+
85
+ Returns:
86
+ str: Python-style format string
42
87
  """
43
88
 
44
- def convert(match_obj, ignore_int):
89
+ def convert(match_obj: re.Match):
45
90
  if match_obj.group() is None:
46
91
  return match_obj
47
- flags, width, precision, length, conversion_type = match_obj.groups()
92
+ flags, width, precision, _, conversion_type = match_obj.groups()
48
93
  format_template = ""
49
94
  if flags:
50
95
  format_template += f"{flags}"
@@ -53,66 +98,13 @@ def format_string_template(format_str, given_values):
53
98
  if precision:
54
99
  format_template += f"{precision}"
55
100
 
56
- if conversion_type:
57
- if any(
58
- [
59
- str(conversion_type).lower() == "f",
60
- str(conversion_type).lower() == "x",
61
- str(conversion_type).lower() == "o",
62
- str(conversion_type).lower() == "e",
63
- ]
64
- ):
65
- format_template += f"{conversion_type}"
66
- elif all([not ignore_int, str(conversion_type).lower() == "d"]):
67
- format_template += f"{conversion_type}"
101
+ if conversion_type and str(conversion_type).lower() in {"f", "x", "o", "e"}:
102
+ format_template += f"{conversion_type}"
68
103
 
69
104
  return "{}" if format_template == "" else "{:" + format_template + "}"
70
105
 
71
- def convert_include_all(match_obj):
72
- return convert(match_obj, ignore_int=False)
73
-
74
- def convert_ignore_int(match_obj):
75
- return convert(match_obj, ignore_int=True)
76
-
77
- # Allowing single, list and tuple inputs
78
- if not isinstance(given_values, (list, tuple)):
79
- values = (given_values,)
80
- elif isinstance(given_values, list):
81
- values = tuple(given_values)
82
- else:
83
- values = given_values
84
-
85
- pattern = r"(?<!%)(?:%%)*%([\-\+0\ \#])?(\d+|\*)?(\.\*|\.\d+)?([hLIw]|l{1,2}|I32|I64)?([cCdiouxXeEfgGaAnpsSZ])"
106
+ pattern = r"(?<!%)(?:%%)*%([\-\+0\ \#])?(\d+|\*)?(\.\*|\.\d+)?([hLIw]|l{1,2}|I32|I64)?([cCdiouxXeEfgGaAnpsSZ])" # NOSONAR
86
107
 
87
108
  match = re.compile(pattern)
88
109
 
89
- # First try to include all types
90
- try:
91
- formatted_str = re.sub(match, convert_include_all, format_str)
92
- result = formatted_str.format(*values)
93
- result = result.replace("%%", "%")
94
- return result
95
- except Exception:
96
- msg = "Value and format string do not match. "
97
- msg += " Will ignore integer flags `d` in string template. "
98
- msg += f"values: {values}. "
99
- msg += f"format_str: {format_str}. "
100
- msg += f"given_values: {given_values}"
101
- LOGGER.warning(msg)
102
-
103
- # Second try by not including %d.
104
- # This will resolve failing ENUMs with %d
105
- # but will fail on other types.
106
- try:
107
- formatted_str = re.sub(match, convert_ignore_int, format_str)
108
- result = formatted_str.format(*values)
109
- result = result.replace("%%", "%")
110
- return result
111
- except ValueError as e:
112
- msg = "Value and format string do not match. "
113
- msg += f"values: {values}. "
114
- msg += f"format_str: {format_str}. "
115
- msg += f"given_values: {given_values}"
116
- msg += f"Err Msg: {str(e)}\n"
117
- LOGGER.error(msg)
118
- raise ValueError
110
+ return re.sub(match, convert, format_str).replace("%%", "%")
@@ -9,6 +9,7 @@ replace the ThreadedTcpServer for several reasons as described below.
9
9
 
10
10
  @author lestarch
11
11
  """
12
+
12
13
  import logging
13
14
  import struct
14
15
  from typing import Tuple
@@ -24,6 +25,7 @@ from fprime_gds.common.transport import (
24
25
 
25
26
  LOGGER = logging.getLogger("transport")
26
27
 
28
+
27
29
  class ZmqWrapper(object):
28
30
  """Handler for ZMQ functions for use in other objects"""
29
31
 
@@ -50,7 +52,9 @@ class ZmqWrapper(object):
50
52
  sub_topic: subscription topic used to filter incoming messages
51
53
  pub_topic: publication topic supplied for remote subscription filters
52
54
  """
53
- assert len(transport_url) == 2, f"Must supply a pair of URLs for ZeroMQ not '{transport_url}'"
55
+ assert (
56
+ len(transport_url) == 2
57
+ ), f"Must supply a pair of URLs for ZeroMQ not '{transport_url}'"
54
58
  self.pub_topic = pub_topic
55
59
  self.sub_topic = sub_topic
56
60
  self.transport_url = transport_url
@@ -78,8 +82,12 @@ class ZmqWrapper(object):
78
82
  The connection is made using self.transport_url, and as such, this must be configured before running. This is
79
83
  intended to be called on the sending thread.
80
84
  """
81
- assert self.transport_url is not None and len(self.transport_url) == 2, "Must configure before connecting"
82
- assert self.zmq_socket_outgoing is None, "Cannot connect outgoing multiple times"
85
+ assert (
86
+ self.transport_url is not None and len(self.transport_url) == 2
87
+ ), "Must configure before connecting"
88
+ assert (
89
+ self.zmq_socket_outgoing is None
90
+ ), "Cannot connect outgoing multiple times"
83
91
  assert self.pub_topic is not None, "Must configure sockets before connecting"
84
92
  self.zmq_socket_outgoing = self.context.socket(zmq.PUB)
85
93
  self.zmq_socket_outgoing.setsockopt(zmq.SNDHWM, 0)
@@ -93,7 +101,7 @@ class ZmqWrapper(object):
93
101
  self.zmq_socket_outgoing.connect(self.transport_url[0])
94
102
 
95
103
  def connect_incoming(self):
96
- """ Sets up a ZeroMQ connection for incoming data
104
+ """Sets up a ZeroMQ connection for incoming data
97
105
 
98
106
  ZeroMQ allows multiple connections to a single endpoint. This only affects incoming connections as sockets must
99
107
  be created on their owning threads. This will connect the ZeroMQ topology and if self.server is set, will bind
@@ -102,8 +110,12 @@ class ZmqWrapper(object):
102
110
  The connection is made using self.transport_url, and as such, this must be configured before running. This is
103
111
  intended to be called on the receiving thread.
104
112
  """
105
- assert self.transport_url is not None and len(self.transport_url) == 2, "Must configure before connecting"
106
- assert self.zmq_socket_incoming is None, "Cannot connect incoming multiple times"
113
+ assert (
114
+ self.transport_url is not None and len(self.transport_url) == 2
115
+ ), "Must configure before connecting"
116
+ assert (
117
+ self.zmq_socket_incoming is None
118
+ ), "Cannot connect incoming multiple times"
107
119
  assert self.sub_topic is not None, "Must configure sockets before connecting"
108
120
  self.zmq_socket_incoming = self.context.socket(zmq.SUB)
109
121
  self.zmq_socket_incoming.setsockopt(zmq.RCVHWM, 0)
@@ -118,14 +130,16 @@ class ZmqWrapper(object):
118
130
 
119
131
  def disconnect_outgoing(self):
120
132
  """Disconnect the ZeroMQ sockets"""
121
- self.zmq_socket_outgoing.close()
133
+ if self.zmq_socket_outgoing is not None:
134
+ self.zmq_socket_outgoing.close()
122
135
 
123
136
  def disconnect_incoming(self):
124
137
  """Disconnect the ZeroMQ sockets"""
125
- self.zmq_socket_incoming.close()
138
+ if self.zmq_socket_incoming is not None:
139
+ self.zmq_socket_incoming.close()
126
140
 
127
141
  def terminate(self):
128
- """ Terminate the ZeroMQ context"""
142
+ """Terminate the ZeroMQ context"""
129
143
  self.context.term()
130
144
 
131
145
  def recv(self, timeout=None):
@@ -162,11 +176,14 @@ class ZmqClient(ThreadedTransportClient):
162
176
  self.zmq = ZmqWrapper()
163
177
 
164
178
  def connect(
165
- self, transport_url: Tuple[str], sub_routing: RoutingTag, pub_routing: RoutingTag
179
+ self,
180
+ transport_url: Tuple[str],
181
+ sub_routing: RoutingTag,
182
+ pub_routing: RoutingTag,
166
183
  ):
167
184
  """Connects to the ZeroMQ network"""
168
185
  self.zmq.configure(transport_url, sub_routing.value, pub_routing.value)
169
- self.zmq.connect_outgoing() # Outgoing socket, for clients, exists on the current thread
186
+ self.zmq.connect_outgoing() # Outgoing socket, for clients, exists on the current thread
170
187
  super().connect(transport_url, sub_routing, pub_routing)
171
188
 
172
189
  def disconnect(self):
@@ -176,16 +193,18 @@ class ZmqClient(ThreadedTransportClient):
176
193
 
177
194
  def send(self, data):
178
195
  """Send data via ZeroMQ"""
179
- if data[:4] == b'ZZZZ':
196
+ if data[:4] == b"ZZZZ":
180
197
  data = data[4:]
181
- self.zmq.send(data) # Must strip out ZZZZ as that is a ThreadedTcpServer only property
198
+ self.zmq.send(
199
+ data
200
+ ) # Must strip out ZZZZ as that is a ThreadedTcpServer only property
182
201
 
183
202
  def recv(self, timeout=None):
184
203
  """Receives data from ZeroMQ"""
185
204
  return self.zmq.recv(timeout)
186
205
 
187
206
  def recv_thread(self):
188
- """ Overrides the recv_thread method
207
+ """Overrides the recv_thread method
189
208
 
190
209
  Overrides the recv_thread method of the superclass such that the ZeroMQ socket may be created/destroyed
191
210
  before/after the main recv loop.
@@ -203,11 +222,11 @@ class ZmqGround(GroundHandler):
203
222
  to the display and processing layer(s). This effectively acts as the "FSW" side of that interface as it
204
223
  frames/deframes packets heading to that layer.
205
224
 
206
- Since there is likely only one communications client to the FSW users should call make_server() after construction
225
+ Since there is likely only one communications client to the FSW users should instantiate with server=True
207
226
  to ensure that it binds to resources for the network. This is not forced in case of multiple FSW connections.
208
227
  """
209
228
 
210
- def __init__(self, transport_url):
229
+ def __init__(self, transport_url, server=True):
211
230
  """Initialize this interface with the transport_url needed to connect
212
231
 
213
232
  Args:
@@ -217,6 +236,8 @@ class ZmqGround(GroundHandler):
217
236
  self.zmq = ZmqWrapper()
218
237
  self.transport_url = transport_url
219
238
  self.timeout = 10
239
+ if server:
240
+ self.zmq.make_server()
220
241
 
221
242
  def open(self):
222
243
  """Open this ground interface. Delegates to the connect method
@@ -242,10 +263,6 @@ class ZmqGround(GroundHandler):
242
263
  self.zmq.disconnect_outgoing()
243
264
  self.zmq.terminate()
244
265
 
245
- def make_server(self):
246
- """Makes it into a server"""
247
- self.zmq.make_server()
248
-
249
266
  def receive_all(self):
250
267
  """Receive all available packets
251
268
 
@@ -0,0 +1,150 @@
1
+ """ fprime_gds.executables.apps: an implementation of start-up apps in fprime
2
+
3
+ There are twp ways to approach start=up applications in fprime. First, is to implement a run method via a subclass of
4
+ `GdsFunction`. This gives the implementor the ability to run anything within the run function that python offers,
5
+ however; this comes with complexity of setting up a new thread/process/isolation to ensure that the plugin does not
6
+ threaten the fprime-gds core functionality and processes.
7
+
8
+ The second method is to inherit from `GdsApp` implementing the `get_process_invocation` function to return the necessary
9
+ command line that will be spun into its own process.
10
+
11
+ @author lestarch
12
+ """
13
+ import subprocess
14
+ from abc import ABC, abstractmethod
15
+ from typing import List, Type
16
+
17
+ from fprime_gds.plugin.definitions import gds_plugin_specification
18
+
19
+
20
+ class GdsBaseFunction(ABC):
21
+ """ Base functionality for pluggable GDS start-up functions
22
+
23
+ GDS start-up functionality is pluggable. This class acts as a base for pluggable functionality supplies helpers to
24
+ the various start-up plugins.
25
+
26
+ Developers who intend to run in an isolated subprocess are strongly encouraged to use `GdsApp` (see below).
27
+ Developers who need flexibility may use GdsFunction.
28
+ """
29
+
30
+ @abstractmethod
31
+ def run(self):
32
+ """ Run the start-up function
33
+
34
+ Run the start-up function unconstrained by the limitations of running in a dedicated subprocess.
35
+
36
+ """
37
+ raise NotImplementedError()
38
+
39
+
40
+ class GdsFunction(GdsBaseFunction, ABC):
41
+ """ Functionality for pluggable GDS start-up functions
42
+
43
+ GDS start-up functionality is pluggable. This class acts as a wide-open implementation of functionality via a single
44
+ `run` callback. Developers have complete control of the start-up functionality. However, this comes at the cost of
45
+ instability in that case of poorly designed functions.
46
+
47
+ Developers who intend to run in an isolated subprocess are strongly encouraged to use `GdsApp` (see below).
48
+
49
+ Plugin developers are required to implement a single function `run`, which must take care of setting up and running
50
+ the start-up function. Developers **must** handle the isolation of this functionality including spinning off a new
51
+ thread, subprocess, etc. Additionally, the developer must define the `register_gds_function_plugin` class method
52
+ annotated with the @gds_plugin_implementation annotation.
53
+
54
+ Standard plug-in functions (get_name, get_arguments) are available should the implementer desire these features.
55
+ Arguments will be supplied to the class's `__init__` function.
56
+ """
57
+
58
+ @classmethod
59
+ @gds_plugin_specification
60
+ def register_gds_function_plugin(cls) -> Type["GdsFunction"]:
61
+ """Register gds start-up functionality
62
+
63
+ Plugin hook for registering a plugin that supplies start-up functionality. This functionality will run on start-up
64
+ of the GDS network.
65
+
66
+ Note: users should return the class, not an instance of the class. Needed arguments for instantiation are
67
+ determined from class methods, solicited via the command line, and provided at construction time to the chosen
68
+ instantiation.
69
+
70
+ Returns:
71
+ GDSFunction subclass
72
+ """
73
+ raise NotImplementedError()
74
+
75
+
76
+ class GdsApp(GdsBaseFunction):
77
+ """ GDS start-up process functionality
78
+
79
+ A pluggable base class used to start a new process as part of the GDS command line invocation. This allows
80
+ developers to add process-isolated functionality to the GDS network.
81
+
82
+ Plugin developers are required to implement the `get_process_invocation` function that returns a list of arguments
83
+ needed to invoke the process via python's `subprocess`. Additionally, the developer must define the
84
+ `register_gds_function_plugin` class method annotated with the @gds_plugin_implementation annotation.
85
+
86
+ Standard plug-in functions (get_name, get_arguments) are available should the implementer desire these features.
87
+ Arguments will be supplied to the class's `__init__` function.
88
+ """
89
+ def __init__(self, **arguments):
90
+ """ Construct the communication applications around the arguments
91
+
92
+ Command line arguments are passed in to match those returned from the `get_arguments` functions.
93
+
94
+ Args:
95
+ arguments: arguments from the command line
96
+ """
97
+ self.process = None
98
+ self.arguments = arguments
99
+
100
+ def run(self):
101
+ """ Run the application as an isolated process
102
+
103
+ GdsFunction objects require an implementation of the `run` command. This implementation will take the arguments
104
+ provided from `get_process_invocation` function and supplies them as an invocation of the isolated subprocess.
105
+ """
106
+ invocation_arguments = self.get_process_invocation()
107
+ self.process = subprocess.Popen(invocation_arguments)
108
+
109
+ def wait(self, timeout=None):
110
+ """ Wait for the app to complete then return the return code
111
+
112
+ Waits (blocking) for the process to complete. Then returns the return code of the underlying process. If timeout
113
+ is non-None then the process will be killed after waiting for the timeout and another wait of timeout will be
114
+ allowed for the killed process to exit.
115
+
116
+ Return:
117
+ return code of the underlying process
118
+ """
119
+ try:
120
+ _, _ = self.process.wait(timeout=timeout)
121
+ except subprocess.TimeoutExpired:
122
+ self.process.kill()
123
+ _, _ = self.process.wait(timeout=timeout)
124
+ return self.process.returncode
125
+
126
+ @abstractmethod
127
+ def get_process_invocation(self) -> List[str]:
128
+ """ Run the start-up function
129
+
130
+ Run the start-up function unconstrained by the limitations of running in a dedicated subprocess.
131
+
132
+ """
133
+ raise NotImplementedError()
134
+
135
+ @classmethod
136
+ @gds_plugin_specification
137
+ def register_gds_app_plugin(cls) -> Type["GdsApp"]:
138
+ """Register a gds start-up application
139
+
140
+ Plugin hook for registering a plugin that supplies start-up functionality. This functionality will run on start-up
141
+ of the GDS network isolated into a dedicated process.
142
+
143
+ Note: users should return the class, not an instance of the class. Needed arguments for instantiation are
144
+ determined from class methods, solicited via the command line, and provided at construction time to the chosen
145
+ instantiation.
146
+
147
+ Returns:
148
+ GdsApp subclass
149
+ """
150
+ raise NotImplementedError()