|
26 | 26 | from typing import Union |
27 | 27 |
|
28 | 28 | from launch.action import Action |
| 29 | +from launch.actions import ExecuteLocal |
29 | 30 | from launch.actions import ExecuteProcess |
30 | 31 | from launch.frontend import Entity |
31 | 32 | from launch.frontend import expose_action |
|
40 | 41 | from launch.utilities import normalize_to_list_of_substitutions |
41 | 42 | from launch.utilities import perform_substitutions |
42 | 43 |
|
| 44 | +from launch_ros.descriptions import Node as NodeDescription |
| 45 | +from launch_ros.descriptions import RosExecutable |
| 46 | +from launch_ros.descriptions import Parameter |
| 47 | +from launch_ros.descriptions import ParameterFile |
43 | 48 | from launch_ros.parameters_type import SomeParameters |
44 | 49 | from launch_ros.remap_rule_type import SomeRemapRules |
45 | 50 | from launch_ros.substitutions import ExecutableInPackage |
|
56 | 61 |
|
57 | 62 | import yaml |
58 | 63 |
|
59 | | -from ..descriptions import Parameter |
60 | | -from ..descriptions import ParameterFile |
61 | | - |
62 | 64 |
|
63 | 65 | @expose_action('node') |
64 | | -class Node(ExecuteProcess): |
| 66 | +class Node(ExecuteLocal): |
65 | 67 | """Action that executes a ROS node.""" |
66 | 68 |
|
67 | | - UNSPECIFIED_NODE_NAME = '<node_name_unspecified>' |
68 | | - UNSPECIFIED_NODE_NAMESPACE = '<node_namespace_unspecified>' |
69 | | - |
70 | 69 | def __init__( |
71 | 70 | self, *, |
72 | 71 | executable: SomeSubstitutionsType, |
@@ -139,42 +138,10 @@ def __init__( |
139 | 138 | passed to the node as ROS remapping rules |
140 | 139 | :param: arguments list of extra arguments for the node |
141 | 140 | """ |
142 | | - if package is not None: |
143 | | - cmd = [ExecutableInPackage(package=package, executable=executable)] |
144 | | - else: |
145 | | - cmd = [executable] |
146 | | - cmd += [] if arguments is None else arguments |
147 | | - # Reserve space for ros specific arguments. |
148 | | - # The substitutions will get expanded when the action is executed. |
149 | | - cmd += ['--ros-args'] # Prepend ros specific arguments with --ros-args flag |
150 | | - if name is not None: |
151 | | - cmd += ['-r', LocalSubstitution( |
152 | | - "ros_specific_arguments['name']", description='node name')] |
153 | | - if parameters is not None: |
154 | | - ensure_argument_type(parameters, (list), 'parameters', 'Node') |
155 | | - # All elements in the list are paths to files with parameters (or substitutions that |
156 | | - # evaluate to paths), or dictionaries of parameters (fields can be substitutions). |
157 | | - normalized_params = normalize_parameters(parameters) |
158 | | - # Forward 'exec_name' as to ExecuteProcess constructor |
159 | | - kwargs['name'] = exec_name |
160 | | - super().__init__(cmd=cmd, **kwargs) |
161 | | - self.__package = package |
162 | | - self.__node_executable = executable |
163 | | - self.__node_name = name |
164 | | - self.__node_namespace = namespace |
165 | | - self.__parameters = [] if parameters is None else normalized_params |
166 | | - self.__remappings = [] if remappings is None else list(normalize_remap_rules(remappings)) |
167 | | - self.__arguments = arguments |
168 | | - |
169 | | - self.__expanded_node_name = self.UNSPECIFIED_NODE_NAME |
170 | | - self.__expanded_node_namespace = self.UNSPECIFIED_NODE_NAMESPACE |
171 | | - self.__expanded_parameter_arguments = None # type: Optional[List[Tuple[Text, bool]]] |
172 | | - self.__final_node_name = None # type: Optional[Text] |
173 | | - self.__expanded_remappings = None # type: Optional[List[Tuple[Text, Text]]] |
174 | 141 |
|
175 | | - self.__substitutions_performed = False |
176 | | - |
177 | | - self.__logger = launch.logging.get_logger(__name__) |
| 142 | + self.__node_desc = NodeDescription(node_name=name, namespace=namespace, parameters=parameters, remappings=remappings, arguments=arguments) |
| 143 | + self.__ros_exec = RosExecutable(package=package, executable_name=executable, nodes=[self.__node_desc]) |
| 144 | + super().__init__(process_description=self.__ros_exec, **kwargs) |
178 | 145 |
|
179 | 146 | @staticmethod |
180 | 147 | def parse_nested_parameters(params, parser): |
@@ -243,10 +210,10 @@ def get_nested_dictionary_from_nested_key_value_pairs(params): |
243 | 210 | def parse(cls, entity: Entity, parser: Parser): |
244 | 211 | """Parse node.""" |
245 | 212 | # See parse method of `ExecuteProcess` |
246 | | - _, kwargs = super().parse(entity, parser, ignore=['cmd']) |
| 213 | + _, kwargs = ExecuteProcess.parse(entity, parser, ignore=['cmd']) |
247 | 214 | args = entity.get_attr('args', optional=True) |
248 | 215 | if args is not None: |
249 | | - kwargs['arguments'] = super()._parse_cmdline(args, parser) |
| 216 | + kwargs['arguments'] = ExecuteProcess._parse_cmdline(args, parser) |
250 | 217 | node_name = entity.get_attr('node-name', optional=True) |
251 | 218 | if node_name is not None: |
252 | 219 | kwargs['node_name'] = parser.parse_substitution(node_name) |
@@ -282,153 +249,16 @@ def parse(cls, entity: Entity, parser: Parser): |
282 | 249 | @property |
283 | 250 | def node_name(self): |
284 | 251 | """Getter for node_name.""" |
285 | | - if self.__final_node_name is None: |
| 252 | + if self.__node_desc.final_node_name is None: |
286 | 253 | raise RuntimeError("cannot access 'node_name' before executing action") |
287 | | - return self.__final_node_name |
288 | | - |
289 | | - def is_node_name_fully_specified(self): |
290 | | - keywords = (self.UNSPECIFIED_NODE_NAME, self.UNSPECIFIED_NODE_NAMESPACE) |
291 | | - return all(x not in self.node_name for x in keywords) |
292 | | - |
293 | | - def _create_params_file_from_dict(self, params): |
294 | | - with NamedTemporaryFile(mode='w', prefix='launch_params_', delete=False) as h: |
295 | | - param_file_path = h.name |
296 | | - param_dict = { |
297 | | - self.node_name if self.is_node_name_fully_specified() else '/**': |
298 | | - {'ros__parameters': params} |
299 | | - } |
300 | | - yaml.dump(param_dict, h, default_flow_style=False) |
301 | | - return param_file_path |
302 | | - |
303 | | - def _get_parameter_rule(self, param: 'Parameter', context: LaunchContext): |
304 | | - name, value = param.evaluate(context) |
305 | | - return f'{name}:={yaml.dump(value)}' |
306 | | - |
307 | | - def _perform_substitutions(self, context: LaunchContext) -> None: |
308 | | - # Here to avoid cyclic import |
309 | | - from ..descriptions import Parameter |
310 | | - try: |
311 | | - if self.__substitutions_performed: |
312 | | - # This function may have already been called by a subclass' `execute`, for example. |
313 | | - return |
314 | | - self.__substitutions_performed = True |
315 | | - if self.__node_name is not None: |
316 | | - self.__expanded_node_name = perform_substitutions( |
317 | | - context, normalize_to_list_of_substitutions(self.__node_name)) |
318 | | - validate_node_name(self.__expanded_node_name) |
319 | | - self.__expanded_node_name.lstrip('/') |
320 | | - expanded_node_namespace: Optional[Text] = None |
321 | | - if self.__node_namespace is not None: |
322 | | - expanded_node_namespace = perform_substitutions( |
323 | | - context, normalize_to_list_of_substitutions(self.__node_namespace)) |
324 | | - base_ns = context.launch_configurations.get('ros_namespace', None) |
325 | | - expanded_node_namespace = make_namespace_absolute( |
326 | | - prefix_namespace(base_ns, expanded_node_namespace)) |
327 | | - if expanded_node_namespace is not None: |
328 | | - self.__expanded_node_namespace = expanded_node_namespace |
329 | | - cmd_extension = ['-r', LocalSubstitution("ros_specific_arguments['ns']")] |
330 | | - self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) |
331 | | - validate_namespace(self.__expanded_node_namespace) |
332 | | - except Exception: |
333 | | - self.__logger.error( |
334 | | - "Error while expanding or validating node name or namespace for '{}':" |
335 | | - .format('package={}, executable={}, name={}, namespace={}'.format( |
336 | | - self.__package, |
337 | | - self.__node_executable, |
338 | | - self.__node_name, |
339 | | - self.__node_namespace, |
340 | | - )) |
341 | | - ) |
342 | | - raise |
343 | | - self.__final_node_name = prefix_namespace( |
344 | | - self.__expanded_node_namespace, self.__expanded_node_name) |
345 | | - # expand global parameters first, |
346 | | - # so they can be overriden with specific parameters of this Node |
347 | | - global_params = context.launch_configurations.get('ros_params', None) |
348 | | - if global_params is not None or self.__parameters is not None: |
349 | | - self.__expanded_parameter_arguments = [] |
350 | | - if global_params is not None: |
351 | | - param_file_path = self._create_params_file_from_dict(global_params) |
352 | | - self.__expanded_parameter_arguments.append((param_file_path, True)) |
353 | | - cmd_extension = ['--params-file', f'{param_file_path}'] |
354 | | - self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) |
355 | | - assert os.path.isfile(param_file_path) |
356 | | - # expand parameters too |
357 | | - if self.__parameters is not None: |
358 | | - evaluated_parameters = evaluate_parameters(context, self.__parameters) |
359 | | - for params in evaluated_parameters: |
360 | | - is_file = False |
361 | | - if isinstance(params, dict): |
362 | | - param_argument = self._create_params_file_from_dict(params) |
363 | | - is_file = True |
364 | | - assert os.path.isfile(param_argument) |
365 | | - elif isinstance(params, pathlib.Path): |
366 | | - param_argument = str(params) |
367 | | - is_file = True |
368 | | - elif isinstance(params, Parameter): |
369 | | - param_argument = self._get_parameter_rule(params, context) |
370 | | - else: |
371 | | - raise RuntimeError('invalid normalized parameters {}'.format(repr(params))) |
372 | | - if is_file and not os.path.isfile(param_argument): |
373 | | - self.__logger.warning( |
374 | | - 'Parameter file path is not a file: {}'.format(param_argument), |
375 | | - ) |
376 | | - continue |
377 | | - self.__expanded_parameter_arguments.append((param_argument, is_file)) |
378 | | - cmd_extension = ['--params-file' if is_file else '-p', f'{param_argument}'] |
379 | | - self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) |
380 | | - # expand remappings too |
381 | | - global_remaps = context.launch_configurations.get('ros_remaps', None) |
382 | | - if global_remaps or self.__remappings: |
383 | | - self.__expanded_remappings = [] |
384 | | - if global_remaps: |
385 | | - self.__expanded_remappings.extend(global_remaps) |
386 | | - if self.__remappings: |
387 | | - self.__expanded_remappings.extend([ |
388 | | - (perform_substitutions(context, src), perform_substitutions(context, dst)) |
389 | | - for src, dst in self.__remappings |
390 | | - ]) |
391 | | - if self.__expanded_remappings: |
392 | | - cmd_extension = [] |
393 | | - for src, dst in self.__expanded_remappings: |
394 | | - cmd_extension.extend(['-r', f'{src}:={dst}']) |
395 | | - self.cmd.extend([normalize_to_list_of_substitutions(x) for x in cmd_extension]) |
396 | | - |
397 | | - def execute(self, context: LaunchContext) -> Optional[List[Action]]: |
398 | | - """ |
399 | | - Execute the action. |
400 | | -
|
401 | | - Delegated to :meth:`launch.actions.ExecuteProcess.execute`. |
402 | | - """ |
403 | | - self._perform_substitutions(context) |
404 | | - # Prepare the ros_specific_arguments list and add it to the context so that the |
405 | | - # LocalSubstitution placeholders added to the the cmd can be expanded using the contents. |
406 | | - ros_specific_arguments: Dict[str, Union[str, List[str]]] = {} |
407 | | - if self.__node_name is not None: |
408 | | - ros_specific_arguments['name'] = '__node:={}'.format(self.__expanded_node_name) |
409 | | - if self.__expanded_node_namespace != '': |
410 | | - ros_specific_arguments['ns'] = '__ns:={}'.format(self.__expanded_node_namespace) |
411 | | - context.extend_locals({'ros_specific_arguments': ros_specific_arguments}) |
412 | | - ret = super().execute(context) |
413 | | - |
414 | | - if self.is_node_name_fully_specified(): |
415 | | - add_node_name(context, self.node_name) |
416 | | - node_name_count = get_node_name_count(context, self.node_name) |
417 | | - if node_name_count > 1: |
418 | | - execute_process_logger = launch.logging.get_logger(self.name) |
419 | | - execute_process_logger.warning( |
420 | | - 'there are now at least {} nodes with the name {} created within this ' |
421 | | - 'launch context'.format(node_name_count, self.node_name) |
422 | | - ) |
423 | | - |
424 | | - return ret |
| 254 | + return self.__node_desc.final_node_name |
425 | 255 |
|
426 | 256 | @property |
427 | 257 | def expanded_node_namespace(self): |
428 | 258 | """Getter for expanded_node_namespace.""" |
429 | | - return self.__expanded_node_namespace |
| 259 | + return self.__node_desc.expanded_node_namespace |
430 | 260 |
|
431 | 261 | @property |
432 | 262 | def expanded_remapping_rules(self): |
433 | 263 | """Getter for expanded_remappings.""" |
434 | | - return self.__expanded_remappings |
| 264 | + return self.__node_desc.expanded_remappings |
0 commit comments