nost-tools 2.0.0__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.

Potentially problematic release.


This version of nost-tools might be problematic. Click here for more details.

@@ -0,0 +1,192 @@
1
+ """
2
+ Provides a base logger application that subscribes and writes all messages to file.
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ from datetime import datetime, timedelta
8
+
9
+ from .application import Application
10
+ from .configuration import ConnectionConfig
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class LoggerApplication(Application):
16
+ """
17
+ Logger NOS-T Application.
18
+
19
+ This object class defines the basic functionality for a NOS-T application
20
+ that subscribes to a specified topic and logs all messages to file.
21
+
22
+ Attributes:
23
+ prefix (str): The test run namespace (prefix)
24
+ simulator (:obj:`Simulator`): Application simulator defined in the *Simulator* class
25
+ app_name (str): Logger application name (default: logger)
26
+ app_description (str): Logger application description (optional)
27
+ log_app (str): Application name to be logged (default: "+")
28
+ log_topic (str): Topic to be logged (default: "#")
29
+ log_dir (str): Directory to write log files (default: ".")
30
+ log_file (:obj:`File`): Current log file
31
+ """
32
+
33
+ def __init__(self, app_name: str = "logger", app_description: str = None):
34
+ """
35
+ Initializes a new logging application.
36
+
37
+ Args:
38
+ app_name (str): application name (default: "logger")
39
+ app_description (str): application description (optional)
40
+ """
41
+ super().__init__(app_name, app_description)
42
+ self.log_topic = None
43
+ self.log_app = None
44
+ self.log_dir = None
45
+ self.log_file = None
46
+
47
+ def start_up(
48
+ self,
49
+ prefix: str,
50
+ config: ConnectionConfig,
51
+ set_offset: bool = None,
52
+ time_status_step: timedelta = None,
53
+ time_status_init: datetime = None,
54
+ shut_down_when_terminated: bool = None,
55
+ time_step: timedelta = None,
56
+ manager_app_name: str = None,
57
+ log_app: str = "+",
58
+ log_topic: str = "#",
59
+ log_dir: str = ".",
60
+ ) -> None:
61
+ """
62
+ Starts up the logger application by connecting to message broker,
63
+ starting a background event loop, subscribing to manager events, and
64
+ registering callback functions.
65
+
66
+ Args:
67
+ prefix (str): The test run namespace (prefix)
68
+ config (:obj:`ConnectionConfig`): The connection configuration
69
+ set_offset (bool): True, if the system clock offset shall be set
70
+ time_status_step (:obj:`timedelta`): Time interval for status messages
71
+ time_status_init (:obj:`datetime`): Initial time for status messages
72
+ shut_down_when_terminated (bool): True, if the application shall shut down when terminated
73
+ time_step (:obj:`timedelta`): Time step for the application
74
+ manager_app_name (str): Manager application name
75
+ log_app (str): Application name to be logged (default: "+")
76
+ log_topic (str): Topic to be logged (default: "#")
77
+ log_dir (str): Directory to write log files (default: ".")
78
+ """
79
+ if (
80
+ set_offset is not None
81
+ and time_status_step is not None
82
+ and time_status_init is not None
83
+ and shut_down_when_terminated is not None
84
+ and time_step is not None
85
+ and manager_app_name is not None
86
+ ):
87
+ self.set_offset = set_offset
88
+ self.time_status_step = time_status_step
89
+ self.time_status_init = time_status_init
90
+ self.shut_down_when_terminated = shut_down_when_terminated
91
+ self.time_step = time_step
92
+ self.manager_app_name = manager_app_name
93
+ else:
94
+ self.config = config
95
+ parameters = getattr(
96
+ self.config.rc.simulation_configuration.execution_parameters,
97
+ "logger_application",
98
+ None,
99
+ )
100
+ self.set_offset = parameters.set_offset
101
+ self.time_status_step = parameters.time_status_step
102
+ self.time_status_init = parameters.time_status_init
103
+ self.shut_down_when_terminated = parameters.shut_down_when_terminated
104
+
105
+ self.log_app = log_app
106
+ self.log_topic = log_topic
107
+ self.log_dir = log_dir
108
+
109
+ # Create log directory if it doesn't exist
110
+ os.makedirs(self.log_dir, exist_ok=True)
111
+
112
+ # Open log file now
113
+ self._open_log_file()
114
+
115
+ # Start up base application
116
+ super().start_up(
117
+ prefix,
118
+ config,
119
+ self.set_offset,
120
+ self.time_status_step,
121
+ self.time_status_init,
122
+ self.shut_down_when_terminated,
123
+ )
124
+
125
+ # Add callback for specified topic(s)
126
+ self.add_message_callback(self.log_app, self.log_topic, self.on_log_message)
127
+
128
+ logger.info(f"Logger {self.app_name} started up and listening for messages.")
129
+
130
+ def shut_down(self) -> None:
131
+ """
132
+ Shuts down the application by stopping the background event loop
133
+ and disconnecting from the message broker.
134
+ """
135
+ # Close the log file if it's open
136
+ self._close_log_file()
137
+
138
+ # Shut down base application
139
+ super().shut_down()
140
+
141
+ def _open_log_file(self) -> None:
142
+ """
143
+ Opens a new log file for writing messages.
144
+ """
145
+ if self.log_file is not None:
146
+ self._close_log_file()
147
+
148
+ ts = (
149
+ str(self.simulator.get_wallclock_time()).replace(" ", "T").replace(":", "-")
150
+ )
151
+ log_filename = os.path.join(self.log_dir, f"{ts}.log")
152
+ self.log_file = open(log_filename, "a")
153
+ self.log_file.write(f"Timestamp,Topic,Payload\n")
154
+ logger.info(f"Logger {self.app_name} opened file {self.log_file.name}.")
155
+
156
+ def _close_log_file(self) -> None:
157
+ """
158
+ Closes the current log file if it's open.
159
+ """
160
+ if self.log_file is not None:
161
+ self.log_file.close()
162
+ logger.info(f"Logger {self.app_name} closed file {self.log_file.name}.")
163
+ self.log_file = None
164
+
165
+ def on_log_message(self, ch, method, properties, body):
166
+ """
167
+ Callback function to log a message received by the logger application.
168
+
169
+ Args:
170
+ ch: The channel object
171
+ method: The method frame
172
+ properties: The message properties
173
+ body: The message body
174
+ """
175
+ if self.log_file is not None:
176
+ try:
177
+ routing_key = method.routing_key
178
+ payload = body.decode("utf-8") if isinstance(body, bytes) else str(body)
179
+
180
+ logger.debug(f"Logger {self.app_name} logging message: {payload}")
181
+
182
+ timestamp = self.simulator.get_wallclock_time()
183
+ self.log_file.write(f"{timestamp},{routing_key},{payload}\n")
184
+ self.log_file.flush() # Ensure data is written immediately
185
+ except Exception as e:
186
+ logger.error(f"Error logging message: {e}")
187
+ else:
188
+ # If log file isn't open, try to reopen it
189
+ self._open_log_file()
190
+ logger.error(f"Logger {self.app_name} had to reopen log file.")
191
+ # Try to log the message again
192
+ self.on_log_message(ch, method, properties, body)
@@ -0,0 +1,261 @@
1
+ """
2
+ Provides a base application that manages communication between a simulator and broker.
3
+ """
4
+
5
+ import logging
6
+ import threading
7
+ import traceback
8
+ from datetime import datetime, timedelta
9
+
10
+ from .application import Application
11
+ from .application_utils import ConnectionConfig
12
+ from .schemas import InitCommand, StartCommand, StopCommand, UpdateCommand
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ManagedApplication(Application):
18
+ """
19
+ Managed NOS-T Application.
20
+
21
+ This object class defines the basic functionality for a NOS-T application
22
+ that utilizes an external Manager to command simulator execution.
23
+
24
+ Attributes:
25
+ prefix (str): execution namespace (prefix)
26
+ simulator (:obj:`Simulator`): simulator
27
+ client (:obj:`Client`): MQTT client
28
+ app_name (str): application name
29
+ app_description (str): application description
30
+ time_status_step (:obj:`timedelta`): scenario duration between time status messages
31
+ time_status_init (:obj:`datetime`): scenario time of first time status message
32
+ time_step (:obj:`timedelta`): scenario time step used in execution
33
+ """
34
+
35
+ def __init__(self, app_name: str, app_description: str = None):
36
+ """
37
+ Initializes a new managed application.
38
+
39
+ Args:
40
+ app_name (str): application name
41
+ app_description (str): application description
42
+ """
43
+ super().__init__(app_name, app_description)
44
+ self.time_step = None
45
+ self._sim_start_time = None
46
+ self._sim_stop_time = None
47
+
48
+ def start_up(
49
+ self,
50
+ prefix: str,
51
+ config: ConnectionConfig,
52
+ set_offset: bool = None,
53
+ time_status_step: timedelta = None,
54
+ time_status_init: datetime = None,
55
+ shut_down_when_terminated: bool = None,
56
+ time_step: timedelta = None,
57
+ manager_app_name: str = None,
58
+ ) -> None:
59
+ """
60
+ Starts up the application by connecting to message broker, starting a background event loop,
61
+ subscribing to manager events, and registering callback functions.
62
+
63
+ Args:
64
+ prefix (str): execution namespace (prefix)
65
+ config (:obj:`ConnectionConfig`): connection configuration
66
+ set_offset (bool): True, if the system clock offset shall be set using a NTP request prior to execution
67
+ time_status_step (:obj:`timedelta`): scenario duration between time status messages
68
+ time_status_init (:obj:`datetime`): scenario time for first time status message
69
+ shut_down_when_terminated (bool): True, if the application should shut down when the simulation is terminated
70
+ time_step (:obj:`timedelta`): scenario time step used in execution (Default: 1 second)
71
+ manager_app_name (str): manager application name (Default: manager)
72
+ """
73
+ if (
74
+ set_offset is not None
75
+ and time_status_step is not None
76
+ and time_status_init is not None
77
+ and shut_down_when_terminated is not None
78
+ and time_step is not None
79
+ and manager_app_name is not None
80
+ ):
81
+ self.set_offset = set_offset
82
+ self.time_status_step = time_status_step
83
+ self.time_status_init = time_status_init
84
+ self.shut_down_when_terminated = shut_down_when_terminated
85
+ self.time_step = time_step
86
+ self.manager_app_name = manager_app_name
87
+ else:
88
+ self.config = config
89
+ parameters = getattr(
90
+ self.config.rc.simulation_configuration.execution_parameters,
91
+ "managed_application",
92
+ None,
93
+ )
94
+ self.set_offset = parameters.set_offset
95
+ self.time_status_step = parameters.time_status_step
96
+ self.time_status_init = parameters.time_status_init
97
+ self.shut_down_when_terminated = parameters.shut_down_when_terminated
98
+ self.time_step = parameters.time_step
99
+ self.manager_app_name = parameters.manager_app_name
100
+
101
+ # start up base application
102
+ super().start_up(
103
+ prefix,
104
+ config,
105
+ self.set_offset,
106
+ self.time_status_step,
107
+ self.time_status_init,
108
+ self.shut_down_when_terminated,
109
+ )
110
+ self.time_step = self.time_step
111
+ self.manager_app_name = self.manager_app_name
112
+
113
+ # Register callback functions
114
+ self.add_message_callback(
115
+ app_name=self.manager_app_name,
116
+ app_topic="init",
117
+ user_callback=self.on_manager_init,
118
+ )
119
+ self.add_message_callback(
120
+ app_name=self.manager_app_name,
121
+ app_topic="start",
122
+ user_callback=self.on_manager_start,
123
+ )
124
+ self.add_message_callback(
125
+ app_name=self.manager_app_name,
126
+ app_topic="stop",
127
+ user_callback=self.on_manager_stop,
128
+ )
129
+ self.add_message_callback(
130
+ app_name=self.manager_app_name,
131
+ app_topic="update",
132
+ user_callback=self.on_manager_update,
133
+ )
134
+
135
+ def shut_down(self) -> None:
136
+ """
137
+ Shuts down the application by stopping the background event loop and disconnecting
138
+ the application from the broker.
139
+ """
140
+ # shut down base application
141
+ super().shut_down()
142
+
143
+ def on_manager_init(self, ch, method, properties, body) -> None:
144
+ """
145
+ Callback function for the managed application to respond to an initilize command sent from the manager.
146
+ Parses the scenario start/end times and signals ready.
147
+
148
+ Args:
149
+ ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
150
+ method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
151
+ properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
152
+ body (bytes): The actual message body sent, containing the message payload.
153
+ """
154
+ try:
155
+ # Parse message payload
156
+ message = body.decode("utf-8")
157
+ params = InitCommand.model_validate_json(message).tasking_parameters
158
+ # update default execution start/end time
159
+ self._sim_start_time = params.sim_start_time
160
+ self._sim_stop_time = params.sim_stop_time
161
+ self.ready()
162
+
163
+ except Exception as e:
164
+ logger.error(
165
+ f"Exception (topic: {method.routing_key}, payload: {message}): {e}"
166
+ )
167
+ print(traceback.format_exc())
168
+
169
+ def on_manager_start(self, ch, method, properties, body) -> None:
170
+ """
171
+ Callback function for the managed application to respond to a start command sent from the manager.
172
+ Parses the scenario start/end time, wallclock epoch, and time scale factor and executes
173
+ the simulator in a background thread.
174
+
175
+ Args:
176
+ ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
177
+ method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
178
+ properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
179
+ body (bytes): The actual message body sent, containing the message payload.
180
+ """
181
+ # Parse message payload
182
+ message = body.decode("utf-8")
183
+ params = StartCommand.model_validate_json(message).tasking_parameters
184
+ logger.info(f"Received start command {params}")
185
+ try:
186
+
187
+ # check for optional start time
188
+ if params.sim_start_time is not None:
189
+ self._sim_start_time = params.sim_start_time
190
+ logger.info(f"Sim start time: {params.sim_start_time}")
191
+ # check for optional end time
192
+ if params.sim_stop_time is not None:
193
+ self._sim_stop_time = params.sim_stop_time
194
+ logger.info(f"Sim stop time: {params.sim_stop_time}")
195
+
196
+ threading.Thread(
197
+ target=self.simulator.execute,
198
+ kwargs={
199
+ "init_time": self._sim_start_time,
200
+ "duration": self._sim_stop_time - self._sim_start_time,
201
+ "time_step": self.time_step,
202
+ "wallclock_epoch": params.start_time,
203
+ "time_scale_factor": params.time_scaling_factor,
204
+ },
205
+ ).start()
206
+
207
+ except Exception as e:
208
+ logger.error(
209
+ f"Exception (topic: {method.routing_key}, payload: {message}): {e}"
210
+ )
211
+ print(traceback.format_exc())
212
+
213
+ def on_manager_stop(self, ch, method, properties, body) -> None:
214
+ """
215
+ Callback function for the managed application ('self') to respond to a stop command sent from the manager.
216
+ Parses the end time and updates the simulator.
217
+
218
+ Args:
219
+ ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
220
+ method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
221
+ properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
222
+ body (bytes): The actual message body sent, containing the message payload.
223
+ """
224
+ try:
225
+ # Parse message payload
226
+ message = body.decode("utf-8")
227
+ params = StopCommand.model_validate_json(message).tasking_parameters
228
+ logger.info(f"Received stop command {message}")
229
+ # update execution end time
230
+ self.simulator.set_end_time(params.sim_stop_time)
231
+ except Exception as e:
232
+ logger.error(
233
+ f"Exception (topic: {method.routing_key}, payload: {message}): {e}"
234
+ )
235
+ print(traceback.format_exc())
236
+
237
+ def on_manager_update(self, ch, method, properties, body) -> None:
238
+ """
239
+ Callback function for the managed application ('self') to respond to an update command sent from the manager.
240
+ Parses the time scaling factor and scenario update time and updates the simulator.
241
+
242
+ Args:
243
+ ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
244
+ method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
245
+ properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
246
+ body (bytes): The actual message body sent, containing the message payload.
247
+ """
248
+ try:
249
+ # Parse message payload
250
+ message = body.decode("utf-8")
251
+ params = UpdateCommand.model_validate_json(message).tasking_parameters
252
+ logger.info(f"Received update command {message}")
253
+ # update execution time scale factor
254
+ self.simulator.set_time_scale_factor(
255
+ params.time_scaling_factor, params.sim_update_time
256
+ )
257
+ except Exception as e:
258
+ logger.error(
259
+ f"Exception (topic: {method.routing_key}, payload: {message}): {e}"
260
+ )
261
+ print(traceback.format_exc())