nost-tools 2.0.0__py3-none-any.whl → 2.0.1__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.
- nost_tools/__init__.py +29 -29
- nost_tools/application.py +800 -793
- nost_tools/application_utils.py +262 -262
- nost_tools/configuration.py +304 -304
- nost_tools/entity.py +73 -73
- nost_tools/errors.py +14 -14
- nost_tools/logger_application.py +192 -192
- nost_tools/managed_application.py +261 -261
- nost_tools/manager.py +472 -472
- nost_tools/observer.py +181 -181
- nost_tools/publisher.py +141 -141
- nost_tools/schemas.py +432 -426
- nost_tools/simulator.py +531 -531
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.1.dist-info}/METADATA +118 -119
- nost_tools-2.0.1.dist-info/RECORD +18 -0
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.1.dist-info}/licenses/LICENSE +29 -29
- nost_tools-2.0.0.dist-info/RECORD +0 -18
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.1.dist-info}/WHEEL +0 -0
- {nost_tools-2.0.0.dist-info → nost_tools-2.0.1.dist-info}/top_level.txt +0 -0
nost_tools/manager.py
CHANGED
|
@@ -1,472 +1,472 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Provides a base manager that coordinates a distributed scenario execution.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import json
|
|
6
|
-
import logging
|
|
7
|
-
import threading
|
|
8
|
-
import time
|
|
9
|
-
import traceback
|
|
10
|
-
from datetime import datetime, timedelta
|
|
11
|
-
from typing import List
|
|
12
|
-
|
|
13
|
-
from pydantic import ValidationError
|
|
14
|
-
|
|
15
|
-
from .application import Application
|
|
16
|
-
from .schemas import (
|
|
17
|
-
InitCommand,
|
|
18
|
-
ReadyStatus,
|
|
19
|
-
StartCommand,
|
|
20
|
-
StopCommand,
|
|
21
|
-
TimeStatus,
|
|
22
|
-
UpdateCommand,
|
|
23
|
-
)
|
|
24
|
-
from .simulator import Mode
|
|
25
|
-
|
|
26
|
-
logger = logging.getLogger(__name__)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class TimeScaleUpdate(object):
|
|
30
|
-
"""
|
|
31
|
-
Provides a scheduled update to the simulation time scale factor by sending a message at the designated sim_update_time
|
|
32
|
-
to change the time_scale_factor to the indicated value.
|
|
33
|
-
|
|
34
|
-
Attributes:
|
|
35
|
-
time_scale_factor (float): scenario seconds per wallclock second
|
|
36
|
-
sim_update_time (:obj:`datetime`): scenario time that the update will occur
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
def __init__(self, time_scale_factor: float, sim_update_time: datetime):
|
|
40
|
-
"""
|
|
41
|
-
Instantiates a new time scale update.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
time_scale_factor (float): scenario seconds per wallclock second
|
|
45
|
-
sim_update_time (:obj:`datetime`): scenario time that the update will occur
|
|
46
|
-
"""
|
|
47
|
-
self.time_scale_factor = time_scale_factor
|
|
48
|
-
self.sim_update_time = sim_update_time
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
class Manager(Application):
|
|
52
|
-
"""
|
|
53
|
-
NOS-T Manager Application.
|
|
54
|
-
|
|
55
|
-
This object class defines a manager to orchestrate test run executions.
|
|
56
|
-
|
|
57
|
-
Attributes:
|
|
58
|
-
prefix (str): The test run namespace (prefix)
|
|
59
|
-
simulator (:obj:`Simulator`): Application simulator
|
|
60
|
-
client (:obj:`Client`): Application MQTT client
|
|
61
|
-
time_step (:obj:`timedelta`): Scenario time step used in execution
|
|
62
|
-
time_status_step (:obj:`timedelta`): Scenario duration between time status messages
|
|
63
|
-
time_status_init (:obj:`datetime`): Scenario time of first time status message
|
|
64
|
-
app_name (str): Test run application name
|
|
65
|
-
app_description (str): Test run application description (optional)
|
|
66
|
-
required_apps_status (dict): Ready status for all required applications
|
|
67
|
-
"""
|
|
68
|
-
|
|
69
|
-
def __init__(self):
|
|
70
|
-
"""
|
|
71
|
-
Initializes a new manager.
|
|
72
|
-
"""
|
|
73
|
-
# call super class constructor
|
|
74
|
-
super().__init__("manager")
|
|
75
|
-
self.required_apps_status = {}
|
|
76
|
-
|
|
77
|
-
self.sim_start_time = None
|
|
78
|
-
self.sim_stop_time = None
|
|
79
|
-
start_time = None
|
|
80
|
-
time_step = None
|
|
81
|
-
time_scale_factor = None
|
|
82
|
-
time_scale_updates = None
|
|
83
|
-
time_status_step = None
|
|
84
|
-
time_status_init = None
|
|
85
|
-
command_lead = None
|
|
86
|
-
required_apps = None
|
|
87
|
-
init_retry_delay_s = None
|
|
88
|
-
init_max_retry = None
|
|
89
|
-
|
|
90
|
-
def establish_exchange(self):
|
|
91
|
-
"""
|
|
92
|
-
Establishes the exchange for the manager application.
|
|
93
|
-
"""
|
|
94
|
-
self.channel.exchange_declare(
|
|
95
|
-
exchange=self.prefix,
|
|
96
|
-
exchange_type="topic",
|
|
97
|
-
durable=False,
|
|
98
|
-
auto_delete=True,
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
def execute_test_plan(
|
|
102
|
-
self,
|
|
103
|
-
sim_start_time: datetime = None,
|
|
104
|
-
sim_stop_time: datetime = None,
|
|
105
|
-
start_time: datetime = None,
|
|
106
|
-
time_step: timedelta = timedelta(seconds=1),
|
|
107
|
-
time_scale_factor: float = 1.0,
|
|
108
|
-
time_scale_updates: List[TimeScaleUpdate] = [],
|
|
109
|
-
time_status_step: timedelta = None,
|
|
110
|
-
time_status_init: datetime = None,
|
|
111
|
-
command_lead: timedelta = timedelta(seconds=0),
|
|
112
|
-
required_apps: List[str] = [],
|
|
113
|
-
init_retry_delay_s: int = 5,
|
|
114
|
-
init_max_retry: int = 5,
|
|
115
|
-
) -> None:
|
|
116
|
-
"""
|
|
117
|
-
A comprehensive command to start a test run execution.
|
|
118
|
-
|
|
119
|
-
Publishes an initialize, start, zero or more updates, and a stop message in one condensed JSON script for testing purposes,
|
|
120
|
-
or consistent test-case runs.
|
|
121
|
-
|
|
122
|
-
Args:
|
|
123
|
-
sim_start_time (:obj:`datetime`): scenario time at which to start execution
|
|
124
|
-
sim_stop_time (:obj:`datetime`): scenario time at which to stop execution
|
|
125
|
-
start_time (:obj:`datetime`): wallclock time at which to start execution (default: now)
|
|
126
|
-
time_step (:obj:`timedelta`): scenario time step used in execution (default: 1 second)
|
|
127
|
-
time_scale_factor (float): scenario seconds per wallclock second (default: 1.0)
|
|
128
|
-
time_scale_updates (list(:obj:`TimeScaleUpdate`)): list of scheduled time scale updates (default: [])
|
|
129
|
-
time_status_step (:obj:`timedelta`): scenario duration between time status messages
|
|
130
|
-
time_status_init (:obj:`datetime`): scenario time of first time status message
|
|
131
|
-
command_lead (:obj:`timedelta`): wallclock lead time between command and action (default: 0 seconds)
|
|
132
|
-
required_apps (list(str)): list of application names required to continue with the execution
|
|
133
|
-
init_retry_delay_s (float): number of seconds to wait between initialization commands while waiting for required applications
|
|
134
|
-
init_max_retry (int): number of initialization commands while waiting for required applications before continuing to execution
|
|
135
|
-
"""
|
|
136
|
-
if sim_start_time is not None and sim_stop_time is not None:
|
|
137
|
-
self.sim_start_time = sim_start_time
|
|
138
|
-
self.sim_stop_time = sim_stop_time
|
|
139
|
-
self.start_time = start_time
|
|
140
|
-
self.time_step = time_step
|
|
141
|
-
self.time_scale_factor = time_scale_factor
|
|
142
|
-
self.time_scale_updates = time_scale_updates
|
|
143
|
-
self.time_status_step = time_status_step
|
|
144
|
-
self.time_status_init = time_status_init
|
|
145
|
-
self.command_lead = command_lead
|
|
146
|
-
self.required_apps = required_apps
|
|
147
|
-
self.init_retry_delay_s = init_retry_delay_s
|
|
148
|
-
self.init_max_retry = init_max_retry
|
|
149
|
-
else:
|
|
150
|
-
if self.config.rc:
|
|
151
|
-
logger.info("Retrieving execution parameters from YAML file.")
|
|
152
|
-
parameters = getattr(
|
|
153
|
-
self.config.rc.simulation_configuration.execution_parameters,
|
|
154
|
-
self.app_name,
|
|
155
|
-
None,
|
|
156
|
-
)
|
|
157
|
-
self.sim_start_time = parameters.sim_start_time
|
|
158
|
-
self.sim_stop_time = parameters.sim_stop_time
|
|
159
|
-
self.start_time = parameters.start_time
|
|
160
|
-
self.time_step = parameters.time_step
|
|
161
|
-
self.time_scale_factor = parameters.time_scale_factor
|
|
162
|
-
self.time_scale_updates = parameters.time_scale_updates
|
|
163
|
-
self.time_status_step = parameters.time_status_step
|
|
164
|
-
self.time_status_init = parameters.time_status_init
|
|
165
|
-
self.command_lead = parameters.command_lead
|
|
166
|
-
# self.required_apps = (
|
|
167
|
-
# self.config.rc.simulation_configuration.execution_parameters.required_apps
|
|
168
|
-
# )
|
|
169
|
-
self.required_apps = [
|
|
170
|
-
app for app in parameters.required_apps if app != self.app_name
|
|
171
|
-
]
|
|
172
|
-
self.init_retry_delay_s = parameters.init_retry_delay_s
|
|
173
|
-
self.init_max_retry = parameters.init_max_retry
|
|
174
|
-
else:
|
|
175
|
-
raise ValueError(
|
|
176
|
-
"No configuration runtime. Please provide simulation start and stop times."
|
|
177
|
-
)
|
|
178
|
-
####
|
|
179
|
-
self.establish_exchange()
|
|
180
|
-
# if self.predefined_exchanges_queues:
|
|
181
|
-
# self.declare_exchange()
|
|
182
|
-
# self.declare_bind_queue()
|
|
183
|
-
####
|
|
184
|
-
|
|
185
|
-
self.required_apps_status = dict(
|
|
186
|
-
zip(self.required_apps, [False] * len(self.required_apps))
|
|
187
|
-
)
|
|
188
|
-
self.add_message_callback("*", "status.ready", self.on_app_ready_status)
|
|
189
|
-
self.add_message_callback("*", "status.time", self.on_app_time_status)
|
|
190
|
-
|
|
191
|
-
self._create_time_status_publisher(self.time_status_step, self.time_status_init)
|
|
192
|
-
for i in range(self.init_max_retry):
|
|
193
|
-
# issue the init command
|
|
194
|
-
self.init(self.sim_start_time, self.sim_stop_time, self.required_apps)
|
|
195
|
-
next_try = self.simulator.get_wallclock_time() + timedelta(
|
|
196
|
-
seconds=self.init_retry_delay_s
|
|
197
|
-
)
|
|
198
|
-
# wait until all required apps are ready
|
|
199
|
-
while (
|
|
200
|
-
not all([self.required_apps_status[app] for app in self.required_apps])
|
|
201
|
-
and self.simulator.get_wallclock_time() < next_try
|
|
202
|
-
):
|
|
203
|
-
time.sleep(0.001)
|
|
204
|
-
# self.remove_message_callback("*", "status.ready")
|
|
205
|
-
# self.remove_message_callback()
|
|
206
|
-
# configure start time
|
|
207
|
-
if self.start_time is None:
|
|
208
|
-
self.start_time = self.simulator.get_wallclock_time() + self.command_lead
|
|
209
|
-
# sleep until the start command needs to be issued
|
|
210
|
-
time.sleep(
|
|
211
|
-
max(
|
|
212
|
-
0,
|
|
213
|
-
(
|
|
214
|
-
(self.start_time - self.simulator.get_wallclock_time())
|
|
215
|
-
- self.command_lead
|
|
216
|
-
)
|
|
217
|
-
/ timedelta(seconds=1),
|
|
218
|
-
)
|
|
219
|
-
)
|
|
220
|
-
# issue the start command
|
|
221
|
-
self.start(
|
|
222
|
-
self.sim_start_time,
|
|
223
|
-
self.sim_stop_time,
|
|
224
|
-
self.start_time,
|
|
225
|
-
self.time_step,
|
|
226
|
-
self.time_scale_factor,
|
|
227
|
-
self.time_status_step,
|
|
228
|
-
self.time_status_init,
|
|
229
|
-
)
|
|
230
|
-
# wait for simulation to start executing
|
|
231
|
-
while self.simulator.get_mode() != Mode.EXECUTING:
|
|
232
|
-
time.sleep(0.001)
|
|
233
|
-
for update in self.time_scale_updates:
|
|
234
|
-
update_time = self.simulator.get_wallclock_time_at_simulation_time(
|
|
235
|
-
update.sim_update_time
|
|
236
|
-
)
|
|
237
|
-
# sleep until the update command needs to be issued
|
|
238
|
-
time.sleep(
|
|
239
|
-
max(
|
|
240
|
-
0,
|
|
241
|
-
(
|
|
242
|
-
(update_time - self.simulator.get_wallclock_time())
|
|
243
|
-
- self.command_lead
|
|
244
|
-
)
|
|
245
|
-
/ timedelta(seconds=1),
|
|
246
|
-
)
|
|
247
|
-
)
|
|
248
|
-
# issue the update command
|
|
249
|
-
self.update(
|
|
250
|
-
update.time_scale_factor, update.sim_update_time, self.required_apps
|
|
251
|
-
)
|
|
252
|
-
# wait until the update command takes effect
|
|
253
|
-
while self.simulator.get_time_scale_factor() != update.time_scale_factor:
|
|
254
|
-
time.sleep(self.command_lead / timedelta(seconds=1) / 100)
|
|
255
|
-
end_time = self.simulator.get_wallclock_time_at_simulation_time(
|
|
256
|
-
self.simulator.get_end_time()
|
|
257
|
-
)
|
|
258
|
-
# sleep until the stop command should be issued
|
|
259
|
-
time.sleep(
|
|
260
|
-
max(
|
|
261
|
-
0,
|
|
262
|
-
((end_time - self.simulator.get_wallclock_time()) - self.command_lead)
|
|
263
|
-
/ timedelta(seconds=1),
|
|
264
|
-
)
|
|
265
|
-
)
|
|
266
|
-
# issue the stop command
|
|
267
|
-
self.stop(self.sim_stop_time)
|
|
268
|
-
|
|
269
|
-
def on_app_ready_status(self, ch, method, properties, body) -> None:
|
|
270
|
-
"""
|
|
271
|
-
Callback to handle a message containing an application ready status.
|
|
272
|
-
|
|
273
|
-
Args:
|
|
274
|
-
ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
|
|
275
|
-
method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
|
|
276
|
-
properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
|
|
277
|
-
body (bytes): The actual message body sent, containing the message payload.
|
|
278
|
-
"""
|
|
279
|
-
try:
|
|
280
|
-
# split the message topic into components (prefix/app_name/...)
|
|
281
|
-
topic_parts = method.routing_key.split(".")
|
|
282
|
-
message = body.decode("utf-8")
|
|
283
|
-
# check if app_name is monitored in the ready_status dict
|
|
284
|
-
if len(topic_parts) > 1 and topic_parts[1] in self.required_apps_status:
|
|
285
|
-
# validate if message is a valid JSON
|
|
286
|
-
try:
|
|
287
|
-
# update the ready status based on the payload value
|
|
288
|
-
self.required_apps_status[topic_parts[1]] = (
|
|
289
|
-
ReadyStatus.model_validate_json(message).properties.ready
|
|
290
|
-
)
|
|
291
|
-
except json.JSONDecodeError:
|
|
292
|
-
logger.error(f"Invalid JSON format: {message}")
|
|
293
|
-
except ValidationError as e:
|
|
294
|
-
logger.error(f"Validation error: {e}")
|
|
295
|
-
except Exception as e:
|
|
296
|
-
logger.error(
|
|
297
|
-
f"Exception (topic: {method.routing_key}, payload: {message}): {e}"
|
|
298
|
-
)
|
|
299
|
-
print(traceback.format_exc())
|
|
300
|
-
|
|
301
|
-
def on_app_time_status(self, ch, method, properties, body) -> None:
|
|
302
|
-
"""
|
|
303
|
-
Callback to handle a message containing an application time status.
|
|
304
|
-
|
|
305
|
-
Args:
|
|
306
|
-
ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
|
|
307
|
-
method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
|
|
308
|
-
properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
|
|
309
|
-
body (bytes): The actual message body sent, containing the message payload.
|
|
310
|
-
"""
|
|
311
|
-
try:
|
|
312
|
-
# split the message topic into components (prefix/app_name/...)
|
|
313
|
-
topic_parts = method.routing_key.split(".")
|
|
314
|
-
message = body.decode("utf-8")
|
|
315
|
-
# validate if message is a valid JSON
|
|
316
|
-
try:
|
|
317
|
-
# parse the message payload properties
|
|
318
|
-
props = TimeStatus.model_validate_json(message).properties
|
|
319
|
-
wallclock_delta = self.simulator.get_wallclock_time() - props.time
|
|
320
|
-
scenario_delta = self.simulator.get_time() - props.sim_time
|
|
321
|
-
if len(topic_parts) > 1:
|
|
322
|
-
logger.info(
|
|
323
|
-
f"Application {topic_parts[1]} latency: {scenario_delta} (scenario), {wallclock_delta} (wallclock)"
|
|
324
|
-
)
|
|
325
|
-
except json.JSONDecodeError:
|
|
326
|
-
logger.error(f"Invalid JSON format: {message}")
|
|
327
|
-
except ValidationError as e:
|
|
328
|
-
logger.error(f"Validation error: {e}")
|
|
329
|
-
except Exception as e:
|
|
330
|
-
logger.error(
|
|
331
|
-
f"Exception (topic: {method.routing_key}, payload: {message}): {e}"
|
|
332
|
-
)
|
|
333
|
-
print(traceback.format_exc())
|
|
334
|
-
|
|
335
|
-
def init(
|
|
336
|
-
self,
|
|
337
|
-
sim_start_time: datetime,
|
|
338
|
-
sim_stop_time: datetime,
|
|
339
|
-
required_apps: List[str] = [],
|
|
340
|
-
) -> None:
|
|
341
|
-
"""
|
|
342
|
-
Publishes an initialize command to initialize a test run execution.
|
|
343
|
-
|
|
344
|
-
Args:
|
|
345
|
-
sim_start_time (:obj:`datetime`): Earliest possible scenario start time
|
|
346
|
-
sim_stop_time (:obj:`datetime`): Latest possible scenario end time
|
|
347
|
-
required_apps (list(str)): List of required apps
|
|
348
|
-
"""
|
|
349
|
-
# publish init command message
|
|
350
|
-
command = InitCommand.model_validate(
|
|
351
|
-
{
|
|
352
|
-
"taskingParameters": {
|
|
353
|
-
"simStartTime": sim_start_time,
|
|
354
|
-
"simStopTime": sim_stop_time,
|
|
355
|
-
"requiredApps": required_apps,
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
)
|
|
359
|
-
logger.info(
|
|
360
|
-
f"Sending initialize command {command.model_dump_json(by_alias=True)}."
|
|
361
|
-
)
|
|
362
|
-
self.send_message(
|
|
363
|
-
app_name=self.app_name,
|
|
364
|
-
app_topics="init",
|
|
365
|
-
payload=command.model_dump_json(by_alias=True),
|
|
366
|
-
)
|
|
367
|
-
# logger.info(f"Declared Queues: {self.declared_queues}")
|
|
368
|
-
# logger.info(f"Declared Exchanges: {self.declared_exchanges}")
|
|
369
|
-
|
|
370
|
-
def start(
|
|
371
|
-
self,
|
|
372
|
-
sim_start_time: datetime,
|
|
373
|
-
sim_stop_time: datetime,
|
|
374
|
-
start_time: datetime = None,
|
|
375
|
-
time_step: timedelta = timedelta(seconds=1),
|
|
376
|
-
time_scale_factor: float = 1.0,
|
|
377
|
-
time_status_step: timedelta = None,
|
|
378
|
-
time_status_init: datetime = None,
|
|
379
|
-
) -> None:
|
|
380
|
-
"""
|
|
381
|
-
|
|
382
|
-
Command to start a test run execution by starting the simulator execution with all necessary parameters and publishing
|
|
383
|
-
a start command, which can be received by the connected applications.
|
|
384
|
-
|
|
385
|
-
Args:
|
|
386
|
-
sim_start_time (:obj:`datetime`): Scenario time at which to start execution
|
|
387
|
-
sim_stop_time (:obj:`datetime`): Scenario time at which to stop execution
|
|
388
|
-
start_time (:obj:`datetime`): Wallclock time at which to start execution (default: now)
|
|
389
|
-
time_step (:obj:`timedelta`): Scenario time step used in execution (default: 1 second)
|
|
390
|
-
time_scale_factor (float): Scenario seconds per wallclock second (default: 1.0)
|
|
391
|
-
time_status_step (:obj:`timedelta`): Scenario duration between time status messages
|
|
392
|
-
time_status_init (:obj:`datetime`): Scenario time of first time status message
|
|
393
|
-
"""
|
|
394
|
-
if start_time is None:
|
|
395
|
-
start_time = self.simulator.get_wallclock_time()
|
|
396
|
-
self.time_status_step = time_status_step
|
|
397
|
-
self.time_status_init = time_status_init
|
|
398
|
-
# publish a start command message
|
|
399
|
-
command = StartCommand.model_validate(
|
|
400
|
-
{
|
|
401
|
-
"taskingParameters": {
|
|
402
|
-
"startTime": start_time,
|
|
403
|
-
"simStartTime": sim_start_time,
|
|
404
|
-
"simStopTime": sim_stop_time,
|
|
405
|
-
"timeScalingFactor": time_scale_factor,
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
)
|
|
409
|
-
logger.info(f"Sending start command {command.model_dump_json(by_alias=True)}.")
|
|
410
|
-
self.send_message(
|
|
411
|
-
app_name=self.app_name,
|
|
412
|
-
app_topics="start",
|
|
413
|
-
payload=command.model_dump_json(by_alias=True),
|
|
414
|
-
)
|
|
415
|
-
exec_thread = threading.Thread(
|
|
416
|
-
target=self.simulator.execute,
|
|
417
|
-
kwargs={
|
|
418
|
-
"init_time": sim_start_time,
|
|
419
|
-
"duration": sim_stop_time - sim_start_time,
|
|
420
|
-
"time_step": time_step,
|
|
421
|
-
"wallclock_epoch": start_time,
|
|
422
|
-
"time_scale_factor": time_scale_factor,
|
|
423
|
-
},
|
|
424
|
-
)
|
|
425
|
-
exec_thread.start()
|
|
426
|
-
|
|
427
|
-
def stop(self, sim_stop_time: datetime) -> None:
|
|
428
|
-
"""
|
|
429
|
-
Command to stop a test run execution by updating the execution end time and publishing a stop command.
|
|
430
|
-
|
|
431
|
-
Args:
|
|
432
|
-
sim_stop_time (:obj:`datetime`): Scenario time at which to stop execution.
|
|
433
|
-
"""
|
|
434
|
-
# publish a stop command message
|
|
435
|
-
command = StopCommand.model_validate(
|
|
436
|
-
{"taskingParameters": {"simStopTime": sim_stop_time}}
|
|
437
|
-
)
|
|
438
|
-
logger.info(f"Sending stop command {command.model_dump_json(by_alias=True)}.")
|
|
439
|
-
self.send_message(
|
|
440
|
-
app_name=self.app_name,
|
|
441
|
-
app_topics="stop",
|
|
442
|
-
payload=command.model_dump_json(by_alias=True),
|
|
443
|
-
)
|
|
444
|
-
# update the execution end time
|
|
445
|
-
self.simulator.set_end_time(sim_stop_time)
|
|
446
|
-
|
|
447
|
-
def update(self, time_scale_factor: float, sim_update_time: datetime) -> None:
|
|
448
|
-
"""
|
|
449
|
-
Command to update the time scaling factor for a test run execution by updating the execution time scale factor,
|
|
450
|
-
and publishing an update command.
|
|
451
|
-
|
|
452
|
-
Args:
|
|
453
|
-
time_scale_factor (float): scenario seconds per wallclock second
|
|
454
|
-
sim_update_time (:obj:`datetime`): scenario time at which to update
|
|
455
|
-
"""
|
|
456
|
-
# publish an update command message
|
|
457
|
-
command = UpdateCommand.model_validate(
|
|
458
|
-
{
|
|
459
|
-
"taskingParameters": {
|
|
460
|
-
"simUpdateTime": sim_update_time,
|
|
461
|
-
"timeScalingFactor": time_scale_factor,
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
)
|
|
465
|
-
logger.info(f"Sending update command {command.model_dump_json(by_alias=True)}.")
|
|
466
|
-
self.send_message(
|
|
467
|
-
app_name=self.app_name,
|
|
468
|
-
app_topics="update",
|
|
469
|
-
payload=command.model_dump_json(by_alias=True),
|
|
470
|
-
)
|
|
471
|
-
# update the execution time scale factor
|
|
472
|
-
self.simulator.set_time_scale_factor(time_scale_factor, sim_update_time)
|
|
1
|
+
"""
|
|
2
|
+
Provides a base manager that coordinates a distributed scenario execution.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
import traceback
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
from pydantic import ValidationError
|
|
14
|
+
|
|
15
|
+
from .application import Application
|
|
16
|
+
from .schemas import (
|
|
17
|
+
InitCommand,
|
|
18
|
+
ReadyStatus,
|
|
19
|
+
StartCommand,
|
|
20
|
+
StopCommand,
|
|
21
|
+
TimeStatus,
|
|
22
|
+
UpdateCommand,
|
|
23
|
+
)
|
|
24
|
+
from .simulator import Mode
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TimeScaleUpdate(object):
|
|
30
|
+
"""
|
|
31
|
+
Provides a scheduled update to the simulation time scale factor by sending a message at the designated sim_update_time
|
|
32
|
+
to change the time_scale_factor to the indicated value.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
time_scale_factor (float): scenario seconds per wallclock second
|
|
36
|
+
sim_update_time (:obj:`datetime`): scenario time that the update will occur
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, time_scale_factor: float, sim_update_time: datetime):
|
|
40
|
+
"""
|
|
41
|
+
Instantiates a new time scale update.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
time_scale_factor (float): scenario seconds per wallclock second
|
|
45
|
+
sim_update_time (:obj:`datetime`): scenario time that the update will occur
|
|
46
|
+
"""
|
|
47
|
+
self.time_scale_factor = time_scale_factor
|
|
48
|
+
self.sim_update_time = sim_update_time
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Manager(Application):
|
|
52
|
+
"""
|
|
53
|
+
NOS-T Manager Application.
|
|
54
|
+
|
|
55
|
+
This object class defines a manager to orchestrate test run executions.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
prefix (str): The test run namespace (prefix)
|
|
59
|
+
simulator (:obj:`Simulator`): Application simulator
|
|
60
|
+
client (:obj:`Client`): Application MQTT client
|
|
61
|
+
time_step (:obj:`timedelta`): Scenario time step used in execution
|
|
62
|
+
time_status_step (:obj:`timedelta`): Scenario duration between time status messages
|
|
63
|
+
time_status_init (:obj:`datetime`): Scenario time of first time status message
|
|
64
|
+
app_name (str): Test run application name
|
|
65
|
+
app_description (str): Test run application description (optional)
|
|
66
|
+
required_apps_status (dict): Ready status for all required applications
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
"""
|
|
71
|
+
Initializes a new manager.
|
|
72
|
+
"""
|
|
73
|
+
# call super class constructor
|
|
74
|
+
super().__init__("manager")
|
|
75
|
+
self.required_apps_status = {}
|
|
76
|
+
|
|
77
|
+
self.sim_start_time = None
|
|
78
|
+
self.sim_stop_time = None
|
|
79
|
+
start_time = None
|
|
80
|
+
time_step = None
|
|
81
|
+
time_scale_factor = None
|
|
82
|
+
time_scale_updates = None
|
|
83
|
+
time_status_step = None
|
|
84
|
+
time_status_init = None
|
|
85
|
+
command_lead = None
|
|
86
|
+
required_apps = None
|
|
87
|
+
init_retry_delay_s = None
|
|
88
|
+
init_max_retry = None
|
|
89
|
+
|
|
90
|
+
def establish_exchange(self):
|
|
91
|
+
"""
|
|
92
|
+
Establishes the exchange for the manager application.
|
|
93
|
+
"""
|
|
94
|
+
self.channel.exchange_declare(
|
|
95
|
+
exchange=self.prefix,
|
|
96
|
+
exchange_type="topic",
|
|
97
|
+
durable=False,
|
|
98
|
+
auto_delete=True,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def execute_test_plan(
|
|
102
|
+
self,
|
|
103
|
+
sim_start_time: datetime = None,
|
|
104
|
+
sim_stop_time: datetime = None,
|
|
105
|
+
start_time: datetime = None,
|
|
106
|
+
time_step: timedelta = timedelta(seconds=1),
|
|
107
|
+
time_scale_factor: float = 1.0,
|
|
108
|
+
time_scale_updates: List[TimeScaleUpdate] = [],
|
|
109
|
+
time_status_step: timedelta = None,
|
|
110
|
+
time_status_init: datetime = None,
|
|
111
|
+
command_lead: timedelta = timedelta(seconds=0),
|
|
112
|
+
required_apps: List[str] = [],
|
|
113
|
+
init_retry_delay_s: int = 5,
|
|
114
|
+
init_max_retry: int = 5,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""
|
|
117
|
+
A comprehensive command to start a test run execution.
|
|
118
|
+
|
|
119
|
+
Publishes an initialize, start, zero or more updates, and a stop message in one condensed JSON script for testing purposes,
|
|
120
|
+
or consistent test-case runs.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
sim_start_time (:obj:`datetime`): scenario time at which to start execution
|
|
124
|
+
sim_stop_time (:obj:`datetime`): scenario time at which to stop execution
|
|
125
|
+
start_time (:obj:`datetime`): wallclock time at which to start execution (default: now)
|
|
126
|
+
time_step (:obj:`timedelta`): scenario time step used in execution (default: 1 second)
|
|
127
|
+
time_scale_factor (float): scenario seconds per wallclock second (default: 1.0)
|
|
128
|
+
time_scale_updates (list(:obj:`TimeScaleUpdate`)): list of scheduled time scale updates (default: [])
|
|
129
|
+
time_status_step (:obj:`timedelta`): scenario duration between time status messages
|
|
130
|
+
time_status_init (:obj:`datetime`): scenario time of first time status message
|
|
131
|
+
command_lead (:obj:`timedelta`): wallclock lead time between command and action (default: 0 seconds)
|
|
132
|
+
required_apps (list(str)): list of application names required to continue with the execution
|
|
133
|
+
init_retry_delay_s (float): number of seconds to wait between initialization commands while waiting for required applications
|
|
134
|
+
init_max_retry (int): number of initialization commands while waiting for required applications before continuing to execution
|
|
135
|
+
"""
|
|
136
|
+
if sim_start_time is not None and sim_stop_time is not None:
|
|
137
|
+
self.sim_start_time = sim_start_time
|
|
138
|
+
self.sim_stop_time = sim_stop_time
|
|
139
|
+
self.start_time = start_time
|
|
140
|
+
self.time_step = time_step
|
|
141
|
+
self.time_scale_factor = time_scale_factor
|
|
142
|
+
self.time_scale_updates = time_scale_updates
|
|
143
|
+
self.time_status_step = time_status_step
|
|
144
|
+
self.time_status_init = time_status_init
|
|
145
|
+
self.command_lead = command_lead
|
|
146
|
+
self.required_apps = required_apps
|
|
147
|
+
self.init_retry_delay_s = init_retry_delay_s
|
|
148
|
+
self.init_max_retry = init_max_retry
|
|
149
|
+
else:
|
|
150
|
+
if self.config.rc:
|
|
151
|
+
logger.info("Retrieving execution parameters from YAML file.")
|
|
152
|
+
parameters = getattr(
|
|
153
|
+
self.config.rc.simulation_configuration.execution_parameters,
|
|
154
|
+
self.app_name,
|
|
155
|
+
None,
|
|
156
|
+
)
|
|
157
|
+
self.sim_start_time = parameters.sim_start_time
|
|
158
|
+
self.sim_stop_time = parameters.sim_stop_time
|
|
159
|
+
self.start_time = parameters.start_time
|
|
160
|
+
self.time_step = parameters.time_step
|
|
161
|
+
self.time_scale_factor = parameters.time_scale_factor
|
|
162
|
+
self.time_scale_updates = parameters.time_scale_updates
|
|
163
|
+
self.time_status_step = parameters.time_status_step
|
|
164
|
+
self.time_status_init = parameters.time_status_init
|
|
165
|
+
self.command_lead = parameters.command_lead
|
|
166
|
+
# self.required_apps = (
|
|
167
|
+
# self.config.rc.simulation_configuration.execution_parameters.required_apps
|
|
168
|
+
# )
|
|
169
|
+
self.required_apps = [
|
|
170
|
+
app for app in parameters.required_apps if app != self.app_name
|
|
171
|
+
]
|
|
172
|
+
self.init_retry_delay_s = parameters.init_retry_delay_s
|
|
173
|
+
self.init_max_retry = parameters.init_max_retry
|
|
174
|
+
else:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
"No configuration runtime. Please provide simulation start and stop times."
|
|
177
|
+
)
|
|
178
|
+
####
|
|
179
|
+
self.establish_exchange()
|
|
180
|
+
# if self.predefined_exchanges_queues:
|
|
181
|
+
# self.declare_exchange()
|
|
182
|
+
# self.declare_bind_queue()
|
|
183
|
+
####
|
|
184
|
+
|
|
185
|
+
self.required_apps_status = dict(
|
|
186
|
+
zip(self.required_apps, [False] * len(self.required_apps))
|
|
187
|
+
)
|
|
188
|
+
self.add_message_callback("*", "status.ready", self.on_app_ready_status)
|
|
189
|
+
self.add_message_callback("*", "status.time", self.on_app_time_status)
|
|
190
|
+
|
|
191
|
+
self._create_time_status_publisher(self.time_status_step, self.time_status_init)
|
|
192
|
+
for i in range(self.init_max_retry):
|
|
193
|
+
# issue the init command
|
|
194
|
+
self.init(self.sim_start_time, self.sim_stop_time, self.required_apps)
|
|
195
|
+
next_try = self.simulator.get_wallclock_time() + timedelta(
|
|
196
|
+
seconds=self.init_retry_delay_s
|
|
197
|
+
)
|
|
198
|
+
# wait until all required apps are ready
|
|
199
|
+
while (
|
|
200
|
+
not all([self.required_apps_status[app] for app in self.required_apps])
|
|
201
|
+
and self.simulator.get_wallclock_time() < next_try
|
|
202
|
+
):
|
|
203
|
+
time.sleep(0.001)
|
|
204
|
+
# self.remove_message_callback("*", "status.ready")
|
|
205
|
+
# self.remove_message_callback()
|
|
206
|
+
# configure start time
|
|
207
|
+
if self.start_time is None:
|
|
208
|
+
self.start_time = self.simulator.get_wallclock_time() + self.command_lead
|
|
209
|
+
# sleep until the start command needs to be issued
|
|
210
|
+
time.sleep(
|
|
211
|
+
max(
|
|
212
|
+
0,
|
|
213
|
+
(
|
|
214
|
+
(self.start_time - self.simulator.get_wallclock_time())
|
|
215
|
+
- self.command_lead
|
|
216
|
+
)
|
|
217
|
+
/ timedelta(seconds=1),
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
# issue the start command
|
|
221
|
+
self.start(
|
|
222
|
+
self.sim_start_time,
|
|
223
|
+
self.sim_stop_time,
|
|
224
|
+
self.start_time,
|
|
225
|
+
self.time_step,
|
|
226
|
+
self.time_scale_factor,
|
|
227
|
+
self.time_status_step,
|
|
228
|
+
self.time_status_init,
|
|
229
|
+
)
|
|
230
|
+
# wait for simulation to start executing
|
|
231
|
+
while self.simulator.get_mode() != Mode.EXECUTING:
|
|
232
|
+
time.sleep(0.001)
|
|
233
|
+
for update in self.time_scale_updates:
|
|
234
|
+
update_time = self.simulator.get_wallclock_time_at_simulation_time(
|
|
235
|
+
update.sim_update_time
|
|
236
|
+
)
|
|
237
|
+
# sleep until the update command needs to be issued
|
|
238
|
+
time.sleep(
|
|
239
|
+
max(
|
|
240
|
+
0,
|
|
241
|
+
(
|
|
242
|
+
(update_time - self.simulator.get_wallclock_time())
|
|
243
|
+
- self.command_lead
|
|
244
|
+
)
|
|
245
|
+
/ timedelta(seconds=1),
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
# issue the update command
|
|
249
|
+
self.update(
|
|
250
|
+
update.time_scale_factor, update.sim_update_time, self.required_apps
|
|
251
|
+
)
|
|
252
|
+
# wait until the update command takes effect
|
|
253
|
+
while self.simulator.get_time_scale_factor() != update.time_scale_factor:
|
|
254
|
+
time.sleep(self.command_lead / timedelta(seconds=1) / 100)
|
|
255
|
+
end_time = self.simulator.get_wallclock_time_at_simulation_time(
|
|
256
|
+
self.simulator.get_end_time()
|
|
257
|
+
)
|
|
258
|
+
# sleep until the stop command should be issued
|
|
259
|
+
time.sleep(
|
|
260
|
+
max(
|
|
261
|
+
0,
|
|
262
|
+
((end_time - self.simulator.get_wallclock_time()) - self.command_lead)
|
|
263
|
+
/ timedelta(seconds=1),
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
# issue the stop command
|
|
267
|
+
self.stop(self.sim_stop_time)
|
|
268
|
+
|
|
269
|
+
def on_app_ready_status(self, ch, method, properties, body) -> None:
|
|
270
|
+
"""
|
|
271
|
+
Callback to handle a message containing an application ready status.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
|
|
275
|
+
method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
|
|
276
|
+
properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
|
|
277
|
+
body (bytes): The actual message body sent, containing the message payload.
|
|
278
|
+
"""
|
|
279
|
+
try:
|
|
280
|
+
# split the message topic into components (prefix/app_name/...)
|
|
281
|
+
topic_parts = method.routing_key.split(".")
|
|
282
|
+
message = body.decode("utf-8")
|
|
283
|
+
# check if app_name is monitored in the ready_status dict
|
|
284
|
+
if len(topic_parts) > 1 and topic_parts[1] in self.required_apps_status:
|
|
285
|
+
# validate if message is a valid JSON
|
|
286
|
+
try:
|
|
287
|
+
# update the ready status based on the payload value
|
|
288
|
+
self.required_apps_status[topic_parts[1]] = (
|
|
289
|
+
ReadyStatus.model_validate_json(message).properties.ready
|
|
290
|
+
)
|
|
291
|
+
except json.JSONDecodeError:
|
|
292
|
+
logger.error(f"Invalid JSON format: {message}")
|
|
293
|
+
except ValidationError as e:
|
|
294
|
+
logger.error(f"Validation error: {e}")
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.error(
|
|
297
|
+
f"Exception (topic: {method.routing_key}, payload: {message}): {e}"
|
|
298
|
+
)
|
|
299
|
+
print(traceback.format_exc())
|
|
300
|
+
|
|
301
|
+
def on_app_time_status(self, ch, method, properties, body) -> None:
|
|
302
|
+
"""
|
|
303
|
+
Callback to handle a message containing an application time status.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
ch (:obj:`pika.channel.Channel`): The channel object used to communicate with the RabbitMQ server.
|
|
307
|
+
method (:obj:`pika.spec.Basic.Deliver`): Delivery-related information such as delivery tag, exchange, and routing key.
|
|
308
|
+
properties (:obj:`pika.BasicProperties`): Message properties including content type, headers, and more.
|
|
309
|
+
body (bytes): The actual message body sent, containing the message payload.
|
|
310
|
+
"""
|
|
311
|
+
try:
|
|
312
|
+
# split the message topic into components (prefix/app_name/...)
|
|
313
|
+
topic_parts = method.routing_key.split(".")
|
|
314
|
+
message = body.decode("utf-8")
|
|
315
|
+
# validate if message is a valid JSON
|
|
316
|
+
try:
|
|
317
|
+
# parse the message payload properties
|
|
318
|
+
props = TimeStatus.model_validate_json(message).properties
|
|
319
|
+
wallclock_delta = self.simulator.get_wallclock_time() - props.time
|
|
320
|
+
scenario_delta = self.simulator.get_time() - props.sim_time
|
|
321
|
+
if len(topic_parts) > 1:
|
|
322
|
+
logger.info(
|
|
323
|
+
f"Application {topic_parts[1]} latency: {scenario_delta} (scenario), {wallclock_delta} (wallclock)"
|
|
324
|
+
)
|
|
325
|
+
except json.JSONDecodeError:
|
|
326
|
+
logger.error(f"Invalid JSON format: {message}")
|
|
327
|
+
except ValidationError as e:
|
|
328
|
+
logger.error(f"Validation error: {e}")
|
|
329
|
+
except Exception as e:
|
|
330
|
+
logger.error(
|
|
331
|
+
f"Exception (topic: {method.routing_key}, payload: {message}): {e}"
|
|
332
|
+
)
|
|
333
|
+
print(traceback.format_exc())
|
|
334
|
+
|
|
335
|
+
def init(
|
|
336
|
+
self,
|
|
337
|
+
sim_start_time: datetime,
|
|
338
|
+
sim_stop_time: datetime,
|
|
339
|
+
required_apps: List[str] = [],
|
|
340
|
+
) -> None:
|
|
341
|
+
"""
|
|
342
|
+
Publishes an initialize command to initialize a test run execution.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
sim_start_time (:obj:`datetime`): Earliest possible scenario start time
|
|
346
|
+
sim_stop_time (:obj:`datetime`): Latest possible scenario end time
|
|
347
|
+
required_apps (list(str)): List of required apps
|
|
348
|
+
"""
|
|
349
|
+
# publish init command message
|
|
350
|
+
command = InitCommand.model_validate(
|
|
351
|
+
{
|
|
352
|
+
"taskingParameters": {
|
|
353
|
+
"simStartTime": sim_start_time,
|
|
354
|
+
"simStopTime": sim_stop_time,
|
|
355
|
+
"requiredApps": required_apps,
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
)
|
|
359
|
+
logger.info(
|
|
360
|
+
f"Sending initialize command {command.model_dump_json(by_alias=True)}."
|
|
361
|
+
)
|
|
362
|
+
self.send_message(
|
|
363
|
+
app_name=self.app_name,
|
|
364
|
+
app_topics="init",
|
|
365
|
+
payload=command.model_dump_json(by_alias=True),
|
|
366
|
+
)
|
|
367
|
+
# logger.info(f"Declared Queues: {self.declared_queues}")
|
|
368
|
+
# logger.info(f"Declared Exchanges: {self.declared_exchanges}")
|
|
369
|
+
|
|
370
|
+
def start(
|
|
371
|
+
self,
|
|
372
|
+
sim_start_time: datetime,
|
|
373
|
+
sim_stop_time: datetime,
|
|
374
|
+
start_time: datetime = None,
|
|
375
|
+
time_step: timedelta = timedelta(seconds=1),
|
|
376
|
+
time_scale_factor: float = 1.0,
|
|
377
|
+
time_status_step: timedelta = None,
|
|
378
|
+
time_status_init: datetime = None,
|
|
379
|
+
) -> None:
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
Command to start a test run execution by starting the simulator execution with all necessary parameters and publishing
|
|
383
|
+
a start command, which can be received by the connected applications.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
sim_start_time (:obj:`datetime`): Scenario time at which to start execution
|
|
387
|
+
sim_stop_time (:obj:`datetime`): Scenario time at which to stop execution
|
|
388
|
+
start_time (:obj:`datetime`): Wallclock time at which to start execution (default: now)
|
|
389
|
+
time_step (:obj:`timedelta`): Scenario time step used in execution (default: 1 second)
|
|
390
|
+
time_scale_factor (float): Scenario seconds per wallclock second (default: 1.0)
|
|
391
|
+
time_status_step (:obj:`timedelta`): Scenario duration between time status messages
|
|
392
|
+
time_status_init (:obj:`datetime`): Scenario time of first time status message
|
|
393
|
+
"""
|
|
394
|
+
if start_time is None:
|
|
395
|
+
start_time = self.simulator.get_wallclock_time()
|
|
396
|
+
self.time_status_step = time_status_step
|
|
397
|
+
self.time_status_init = time_status_init
|
|
398
|
+
# publish a start command message
|
|
399
|
+
command = StartCommand.model_validate(
|
|
400
|
+
{
|
|
401
|
+
"taskingParameters": {
|
|
402
|
+
"startTime": start_time,
|
|
403
|
+
"simStartTime": sim_start_time,
|
|
404
|
+
"simStopTime": sim_stop_time,
|
|
405
|
+
"timeScalingFactor": time_scale_factor,
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
)
|
|
409
|
+
logger.info(f"Sending start command {command.model_dump_json(by_alias=True)}.")
|
|
410
|
+
self.send_message(
|
|
411
|
+
app_name=self.app_name,
|
|
412
|
+
app_topics="start",
|
|
413
|
+
payload=command.model_dump_json(by_alias=True),
|
|
414
|
+
)
|
|
415
|
+
exec_thread = threading.Thread(
|
|
416
|
+
target=self.simulator.execute,
|
|
417
|
+
kwargs={
|
|
418
|
+
"init_time": sim_start_time,
|
|
419
|
+
"duration": sim_stop_time - sim_start_time,
|
|
420
|
+
"time_step": time_step,
|
|
421
|
+
"wallclock_epoch": start_time,
|
|
422
|
+
"time_scale_factor": time_scale_factor,
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
exec_thread.start()
|
|
426
|
+
|
|
427
|
+
def stop(self, sim_stop_time: datetime) -> None:
|
|
428
|
+
"""
|
|
429
|
+
Command to stop a test run execution by updating the execution end time and publishing a stop command.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
sim_stop_time (:obj:`datetime`): Scenario time at which to stop execution.
|
|
433
|
+
"""
|
|
434
|
+
# publish a stop command message
|
|
435
|
+
command = StopCommand.model_validate(
|
|
436
|
+
{"taskingParameters": {"simStopTime": sim_stop_time}}
|
|
437
|
+
)
|
|
438
|
+
logger.info(f"Sending stop command {command.model_dump_json(by_alias=True)}.")
|
|
439
|
+
self.send_message(
|
|
440
|
+
app_name=self.app_name,
|
|
441
|
+
app_topics="stop",
|
|
442
|
+
payload=command.model_dump_json(by_alias=True),
|
|
443
|
+
)
|
|
444
|
+
# update the execution end time
|
|
445
|
+
self.simulator.set_end_time(sim_stop_time)
|
|
446
|
+
|
|
447
|
+
def update(self, time_scale_factor: float, sim_update_time: datetime) -> None:
|
|
448
|
+
"""
|
|
449
|
+
Command to update the time scaling factor for a test run execution by updating the execution time scale factor,
|
|
450
|
+
and publishing an update command.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
time_scale_factor (float): scenario seconds per wallclock second
|
|
454
|
+
sim_update_time (:obj:`datetime`): scenario time at which to update
|
|
455
|
+
"""
|
|
456
|
+
# publish an update command message
|
|
457
|
+
command = UpdateCommand.model_validate(
|
|
458
|
+
{
|
|
459
|
+
"taskingParameters": {
|
|
460
|
+
"simUpdateTime": sim_update_time,
|
|
461
|
+
"timeScalingFactor": time_scale_factor,
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
logger.info(f"Sending update command {command.model_dump_json(by_alias=True)}.")
|
|
466
|
+
self.send_message(
|
|
467
|
+
app_name=self.app_name,
|
|
468
|
+
app_topics="update",
|
|
469
|
+
payload=command.model_dump_json(by_alias=True),
|
|
470
|
+
)
|
|
471
|
+
# update the execution time scale factor
|
|
472
|
+
self.simulator.set_time_scale_factor(time_scale_factor, sim_update_time)
|