|
14 | 14 |
|
15 | 15 | """Module for the Node action.""" |
16 | 16 |
|
17 | | -import os |
18 | | -import pathlib |
19 | | -from tempfile import NamedTemporaryFile |
20 | 17 | from typing import Dict |
21 | 18 | from typing import Iterable |
22 | 19 | from typing import List |
23 | 20 | from typing import Optional |
24 | 21 | from typing import Text # noqa: F401 |
25 | 22 | from typing import Tuple # noqa: F401 |
26 | | -from typing import Union |
27 | 23 |
|
28 | 24 | try: |
29 | 25 | import importlib.metadata as importlib_metadata |
30 | 26 | except ModuleNotFoundError: |
31 | 27 | import importlib_metadata |
32 | 28 |
|
33 | | -from launch.action import Action |
| 29 | +from launch.actions import ExecuteLocal |
34 | 30 | from launch.actions import ExecuteProcess |
35 | 31 | from launch.frontend import Entity |
36 | 32 | from launch.frontend import expose_action |
37 | 33 | from launch.frontend import Parser |
38 | 34 | from launch.frontend.type_utils import get_data_type_from_identifier |
39 | 35 |
|
40 | 36 | from launch.launch_context import LaunchContext |
41 | | -import launch.logging |
42 | 37 | from launch.some_substitutions_type import SomeSubstitutionsType |
43 | | -from launch.substitutions import LocalSubstitution |
44 | | -from launch.utilities import ensure_argument_type |
45 | | -from launch.utilities import normalize_to_list_of_substitutions |
46 | | -from launch.utilities import perform_substitutions |
47 | 38 |
|
| 39 | +from launch_ros.descriptions import Node as NodeDescription |
| 40 | +from launch_ros.descriptions import ParameterFile |
| 41 | +from launch_ros.descriptions import RosExecutable |
48 | 42 | from launch_ros.parameters_type import SomeParameters |
49 | 43 | from launch_ros.remap_rule_type import SomeRemapRules |
50 | | -from launch_ros.substitutions import ExecutableInPackage |
51 | | -from launch_ros.utilities import add_node_name |
52 | | -from launch_ros.utilities import evaluate_parameters |
53 | | -from launch_ros.utilities import get_node_name_count |
54 | | -from launch_ros.utilities import make_namespace_absolute |
55 | | -from launch_ros.utilities import normalize_parameters |
56 | | -from launch_ros.utilities import normalize_remap_rules |
57 | | -from launch_ros.utilities import plugin_support |
58 | | -from launch_ros.utilities import prefix_namespace |
59 | | - |
60 | | - |
61 | | -from rclpy.validate_namespace import validate_namespace |
62 | | -from rclpy.validate_node_name import validate_node_name |
63 | | - |
64 | | -import yaml |
65 | | - |
66 | | -from ..descriptions import Parameter |
67 | | -from ..descriptions import ParameterFile |
68 | 44 |
|
69 | 45 |
|
70 | 46 | class NodeActionExtension: |
@@ -110,12 +86,9 @@ def prepare_for_execute(self, context, ros_specific_arguments, node_action): |
110 | 86 |
|
111 | 87 |
|
112 | 88 | @expose_action('node') |
113 | | -class Node(ExecuteProcess): |
| 89 | +class Node(ExecuteLocal): |
114 | 90 | """Action that executes a ROS node.""" |
115 | 91 |
|
116 | | - UNSPECIFIED_NODE_NAME = '<node_name_unspecified>' |
117 | | - UNSPECIFIED_NODE_NAMESPACE = '<node_namespace_unspecified>' |
118 | | - |
119 | 92 | def __init__( |
120 | 93 | self, *, |
121 | 94 | executable: SomeSubstitutionsType, |
@@ -198,46 +171,20 @@ def __init__( |
198 | 171 | :param: ros_arguments list of ROS arguments for the node |
199 | 172 | :param: arguments list of extra arguments for the node |
200 | 173 | """ |
201 | | - if package is not None: |
202 | | - cmd = [ExecutableInPackage(package=package, executable=executable)] |
203 | | - else: |
204 | | - cmd = [executable] |
205 | | - cmd += [] if arguments is None else arguments |
206 | | - cmd += [] if ros_arguments is None else ['--ros-args'] + ros_arguments |
207 | | - # Reserve space for ros specific arguments. |
208 | | - # The substitutions will get expanded when the action is executed. |
209 | | - cmd += ['--ros-args'] # Prepend ros specific arguments with --ros-args flag |
210 | | - if name is not None: |
211 | | - cmd += ['-r', LocalSubstitution( |
212 | | - "ros_specific_arguments['name']", description='node name')] |
213 | | - if parameters is not None: |
214 | | - ensure_argument_type(parameters, (list), 'parameters', 'Node') |
215 | | - # All elements in the list are paths to files with parameters (or substitutions that |
216 | | - # evaluate to paths), or dictionaries of parameters (fields can be substitutions). |
217 | | - normalized_params = normalize_parameters(parameters) |
218 | | - # Forward 'exec_name' as to ExecuteProcess constructor |
219 | | - kwargs['name'] = exec_name |
220 | | - super().__init__(cmd=cmd, **kwargs) |
221 | | - self.__package = package |
222 | | - self.__node_executable = executable |
223 | | - self.__node_name = name |
224 | | - self.__node_namespace = namespace |
225 | | - self.__parameters = [] if parameters is None else normalized_params |
226 | | - self.__remappings = [] if remappings is None else list(normalize_remap_rules(remappings)) |
227 | | - self.__ros_arguments = ros_arguments |
228 | | - self.__arguments = arguments |
229 | | - |
230 | | - self.__expanded_node_name = self.UNSPECIFIED_NODE_NAME |
231 | | - self.__expanded_node_namespace = self.UNSPECIFIED_NODE_NAMESPACE |
232 | | - self.__expanded_parameter_arguments = None # type: Optional[List[Tuple[Text, bool]]] |
233 | | - self.__final_node_name = None # type: Optional[Text] |
234 | | - self.__expanded_remappings = None # type: Optional[List[Tuple[Text, Text]]] |
235 | | - |
236 | | - self.__substitutions_performed = False |
237 | | - |
238 | | - self.__logger = launch.logging.get_logger(__name__) |
239 | | - |
| 174 | + self.__node_desc = NodeDescription(node_name=name, node_namespace=namespace, |
| 175 | + parameters=parameters, remappings=remappings, |
| 176 | + arguments=arguments) |
| 177 | + self.__ros_exec = RosExecutable(package=package, executable=executable, |
| 178 | + nodes=[self.__node_desc]) |
240 | 179 | self.__extensions = get_extensions(self.__logger) |
| 180 | + super().__init__(process_description=self.__ros_exec, **kwargs) |
| 181 | + |
| 182 | + def _perform_substitutions(self, lc: LaunchContext): |
| 183 | + self.__node_desc.prepare(lc, self.__ros_exec) |
| 184 | + |
| 185 | + def is_node_name_fully_specified(self): |
| 186 | + return self.__node_desc.is_node_name_fully_specified() |
| 187 | + |
241 | 188 |
|
242 | 189 | @staticmethod |
243 | 190 | def parse_nested_parameters(params, parser): |
@@ -306,7 +253,7 @@ def get_nested_dictionary_from_nested_key_value_pairs(params): |
306 | 253 | def parse(cls, entity: Entity, parser: Parser): |
307 | 254 | """Parse node.""" |
308 | 255 | # See parse method of `ExecuteProcess` |
309 | | - _, kwargs = super().parse(entity, parser, ignore=['cmd']) |
| 256 | + _, kwargs = ExecuteProcess.parse(entity, parser, ignore=['cmd']) |
310 | 257 | args = entity.get_attr('args', optional=True) |
311 | 258 | if args is not None: |
312 | 259 | kwargs['arguments'] = super()._parse_cmdline(args, parser) |
@@ -348,187 +295,27 @@ def parse(cls, entity: Entity, parser: Parser): |
348 | 295 | @property |
349 | 296 | def node_package(self): |
350 | 297 | """Getter for node_package.""" |
351 | | - return self.__package |
| 298 | + return self.__node_exec.package |
352 | 299 |
|
353 | 300 | @property |
354 | 301 | def node_executable(self): |
355 | 302 | """Getter for node_executable.""" |
356 | | - return self.__node_executable |
| 303 | + return self.__node_exec.executable |
357 | 304 |
|
358 | 305 | @property |
359 | 306 | def node_name(self): |
360 | 307 | """Getter for node_name.""" |
361 | | - if self.__final_node_name is None: |
362 | | - raise RuntimeError("cannot access 'node_name' before executing action") |
363 | | - return self.__final_node_name |
364 | | - |
365 | | - def is_node_name_fully_specified(self): |
366 | | - keywords = (self.UNSPECIFIED_NODE_NAME, self.UNSPECIFIED_NODE_NAMESPACE) |
367 | | - return all(x not in self.node_name for x in keywords) |
368 | | - |
369 | | - def _create_params_file_from_dict(self, params): |
370 | | - with NamedTemporaryFile(mode='w', prefix='launch_params_', delete=False) as h: |
371 | | - param_file_path = h.name |
372 | | - param_dict = { |
373 | | - self.node_name if self.is_node_name_fully_specified() else '/**': |
374 | | - {'ros__parameters': params} |
375 | | - } |
376 | | - yaml.dump(param_dict, h, default_flow_style=False) |
377 | | - return param_file_path |
378 | | - |
379 | | - def _get_parameter_rule(self, param: 'Parameter', context: LaunchContext): |
380 | | - name, value = param.evaluate(context) |
381 | | - return f'{name}:={yaml.dump(value)}' |
382 | | - |
383 | | - def _perform_substitutions(self, context: LaunchContext) -> None: |
384 | | - # Here to avoid cyclic import |
385 | | - from ..descriptions import Parameter |
386 | | - try: |
387 | | - if self.__substitutions_performed: |
388 | | - # This function may have already been called by a subclass' `execute`, for example. |
389 | | - return |
390 | | - self.__substitutions_performed = True |
391 | | - if self.__node_name is not None: |
392 | | - self.__expanded_node_name = perform_substitutions( |
393 | | - context, normalize_to_list_of_substitutions(self.__node_name)) |
394 | | - validate_node_name(self.__expanded_node_name) |
395 | | - self.__expanded_node_name.lstrip('/') |
396 | | - expanded_node_namespace: Optional[Text] = None |
397 | | - if self.__node_namespace is not None: |
398 | | - expanded_node_namespace = perform_substitutions( |
399 | | - context, normalize_to_list_of_substitutions(self.__node_namespace)) |
400 | | - base_ns = context.launch_configurations.get('ros_namespace', None) |
401 | | - expanded_node_namespace = make_namespace_absolute( |
402 | | - prefix_namespace(base_ns, expanded_node_namespace)) |
403 | | - if expanded_node_namespace is not None: |
404 | | - self.__expanded_node_namespace = expanded_node_namespace |
405 | | - cmd_extension = ['-r', LocalSubstitution("ros_specific_arguments['ns']")] |
406 | | - self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) |
407 | | - validate_namespace(self.__expanded_node_namespace) |
408 | | - except Exception: |
409 | | - self.__logger.error( |
410 | | - "Error while expanding or validating node name or namespace for '{}':" |
411 | | - .format('package={}, executable={}, name={}, namespace={}'.format( |
412 | | - self.__package, |
413 | | - self.__node_executable, |
414 | | - self.__node_name, |
415 | | - self.__node_namespace, |
416 | | - )) |
417 | | - ) |
418 | | - raise |
419 | | - self.__final_node_name = prefix_namespace( |
420 | | - self.__expanded_node_namespace, self.__expanded_node_name) |
421 | | - |
422 | | - # Expand global parameters first, |
423 | | - # so they can be overridden with specific parameters of this Node |
424 | | - # The params_container list is expected to contain name-value pairs (tuples) |
425 | | - # and/or strings representing paths to parameter files. |
426 | | - params_container = context.launch_configurations.get('global_params', None) |
427 | | - |
428 | | - if any(x is not None for x in (params_container, self.__parameters)): |
429 | | - self.__expanded_parameter_arguments = [] |
430 | | - if params_container is not None: |
431 | | - for param in params_container: |
432 | | - if isinstance(param, tuple): |
433 | | - name, value = param |
434 | | - cmd_extension = ['-p', f'{name}:={value}'] |
435 | | - self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) |
436 | | - else: |
437 | | - param_file_path = os.path.abspath(param) |
438 | | - self.__expanded_parameter_arguments.append((param_file_path, True)) |
439 | | - cmd_extension = ['--params-file', f'{param_file_path}'] |
440 | | - assert os.path.isfile(param_file_path) |
441 | | - self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) |
442 | | - |
443 | | - # expand parameters too |
444 | | - if self.__parameters is not None: |
445 | | - evaluated_parameters = evaluate_parameters(context, self.__parameters) |
446 | | - for params in evaluated_parameters: |
447 | | - is_file = False |
448 | | - if isinstance(params, dict): |
449 | | - param_argument = self._create_params_file_from_dict(params) |
450 | | - is_file = True |
451 | | - assert os.path.isfile(param_argument) |
452 | | - elif isinstance(params, pathlib.Path): |
453 | | - param_argument = str(params) |
454 | | - is_file = True |
455 | | - elif isinstance(params, Parameter): |
456 | | - param_argument = self._get_parameter_rule(params, context) |
457 | | - else: |
458 | | - raise RuntimeError('invalid normalized parameters {}'.format(repr(params))) |
459 | | - if is_file and not os.path.isfile(param_argument): |
460 | | - self.__logger.warning( |
461 | | - 'Parameter file path is not a file: {}'.format(param_argument), |
462 | | - ) |
463 | | - continue |
464 | | - self.__expanded_parameter_arguments.append((param_argument, is_file)) |
465 | | - cmd_extension = ['--params-file' if is_file else '-p', f'{param_argument}'] |
466 | | - self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) |
467 | | - # expand remappings too |
468 | | - global_remaps = context.launch_configurations.get('ros_remaps', None) |
469 | | - if global_remaps or self.__remappings: |
470 | | - self.__expanded_remappings = [] |
471 | | - if global_remaps: |
472 | | - self.__expanded_remappings.extend(global_remaps) |
473 | | - if self.__remappings: |
474 | | - self.__expanded_remappings.extend([ |
475 | | - (perform_substitutions(context, src), perform_substitutions(context, dst)) |
476 | | - for src, dst in self.__remappings |
477 | | - ]) |
478 | | - if self.__expanded_remappings: |
479 | | - cmd_extension = [] |
480 | | - for src, dst in self.__expanded_remappings: |
481 | | - cmd_extension.extend(['-r', f'{src}:={dst}']) |
482 | | - self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) |
483 | | - |
484 | | - def execute(self, context: LaunchContext) -> Optional[List[Action]]: |
485 | | - """ |
486 | | - Execute the action. |
487 | | -
|
488 | | - Delegated to :meth:`launch.actions.ExecuteProcess.execute`. |
489 | | - """ |
490 | | - self._perform_substitutions(context) |
491 | | - # Prepare the ros_specific_arguments list and add it to the context so that the |
492 | | - # LocalSubstitution placeholders added to the the cmd can be expanded using the contents. |
493 | | - ros_specific_arguments: Dict[str, Union[str, List[str]]] = {} |
494 | | - if self.__node_name is not None: |
495 | | - ros_specific_arguments['name'] = '__node:={}'.format(self.__expanded_node_name) |
496 | | - if self.__expanded_node_namespace != '': |
497 | | - ros_specific_arguments['ns'] = '__ns:={}'.format(self.__expanded_node_namespace) |
498 | | - |
499 | | - # Give extensions a chance to prepare for execution |
500 | | - for extension in self.__extensions.values(): |
501 | | - cmd_extension, ros_specific_arguments = extension.prepare_for_execute( |
502 | | - context, |
503 | | - ros_specific_arguments, |
504 | | - self |
505 | | - ) |
506 | | - self.cmd.extend(cmd_extension) |
507 | | - |
508 | | - context.extend_locals({'ros_specific_arguments': ros_specific_arguments}) |
509 | | - ret = super().execute(context) |
510 | | - |
511 | | - if self.is_node_name_fully_specified(): |
512 | | - add_node_name(context, self.node_name) |
513 | | - node_name_count = get_node_name_count(context, self.node_name) |
514 | | - if node_name_count > 1: |
515 | | - execute_process_logger = launch.logging.get_logger(self.name) |
516 | | - execute_process_logger.warning( |
517 | | - 'there are now at least {} nodes with the name {} created within this ' |
518 | | - 'launch context'.format(node_name_count, self.node_name) |
519 | | - ) |
520 | | - |
521 | | - return ret |
| 308 | + return self.__node_desc.node_name |
522 | 309 |
|
523 | 310 | @property |
524 | 311 | def expanded_node_namespace(self): |
525 | 312 | """Getter for expanded_node_namespace.""" |
526 | | - return self.__expanded_node_namespace |
| 313 | + return self.__node_desc.expanded_node_namespace |
527 | 314 |
|
528 | 315 | @property |
529 | 316 | def expanded_remapping_rules(self): |
530 | 317 | """Getter for expanded_remappings.""" |
531 | | - return self.__expanded_remappings |
| 318 | + return self.__node_desc.expanded_remappings |
532 | 319 |
|
533 | 320 |
|
534 | 321 | def instantiate_extension( |
|
0 commit comments