Skip to content

Commit 06d049e

Browse files
author
Roger Strain
committed
Backup checkin
1 parent 2930378 commit 06d049e

File tree

6 files changed

+451
-186
lines changed

6 files changed

+451
-186
lines changed

launch_ros/launch_ros/actions/node.py

Lines changed: 15 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from typing import Union
2727

2828
from launch.action import Action
29+
from launch.actions import ExecuteLocal
2930
from launch.actions import ExecuteProcess
3031
from launch.frontend import Entity
3132
from launch.frontend import expose_action
@@ -40,6 +41,10 @@
4041
from launch.utilities import normalize_to_list_of_substitutions
4142
from launch.utilities import perform_substitutions
4243

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
4348
from launch_ros.parameters_type import SomeParameters
4449
from launch_ros.remap_rule_type import SomeRemapRules
4550
from launch_ros.substitutions import ExecutableInPackage
@@ -56,17 +61,11 @@
5661

5762
import yaml
5863

59-
from ..descriptions import Parameter
60-
from ..descriptions import ParameterFile
61-
6264

6365
@expose_action('node')
64-
class Node(ExecuteProcess):
66+
class Node(ExecuteLocal):
6567
"""Action that executes a ROS node."""
6668

67-
UNSPECIFIED_NODE_NAME = '<node_name_unspecified>'
68-
UNSPECIFIED_NODE_NAMESPACE = '<node_namespace_unspecified>'
69-
7069
def __init__(
7170
self, *,
7271
executable: SomeSubstitutionsType,
@@ -139,42 +138,10 @@ def __init__(
139138
passed to the node as ROS remapping rules
140139
:param: arguments list of extra arguments for the node
141140
"""
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]]]
174141

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)
178145

179146
@staticmethod
180147
def parse_nested_parameters(params, parser):
@@ -243,10 +210,10 @@ def get_nested_dictionary_from_nested_key_value_pairs(params):
243210
def parse(cls, entity: Entity, parser: Parser):
244211
"""Parse node."""
245212
# See parse method of `ExecuteProcess`
246-
_, kwargs = super().parse(entity, parser, ignore=['cmd'])
213+
_, kwargs = ExecuteProcess.parse(entity, parser, ignore=['cmd'])
247214
args = entity.get_attr('args', optional=True)
248215
if args is not None:
249-
kwargs['arguments'] = super()._parse_cmdline(args, parser)
216+
kwargs['arguments'] = ExecuteProcess._parse_cmdline(args, parser)
250217
node_name = entity.get_attr('node-name', optional=True)
251218
if node_name is not None:
252219
kwargs['node_name'] = parser.parse_substitution(node_name)
@@ -282,153 +249,16 @@ def parse(cls, entity: Entity, parser: Parser):
282249
@property
283250
def node_name(self):
284251
"""Getter for node_name."""
285-
if self.__final_node_name is None:
252+
if self.__node_desc.final_node_name is None:
286253
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
425255

426256
@property
427257
def expanded_node_namespace(self):
428258
"""Getter for expanded_node_namespace."""
429-
return self.__expanded_node_namespace
259+
return self.__node_desc.expanded_node_namespace
430260

431261
@property
432262
def expanded_remapping_rules(self):
433263
"""Getter for expanded_remappings."""
434-
return self.__expanded_remappings
264+
return self.__node_desc.expanded_remappings

launch_ros/launch_ros/descriptions/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,20 @@
1515
"""descriptions Module."""
1616

1717
from .composable_node import ComposableNode
18+
from .node import Node
19+
from .node_trait import NodeTrait
20+
from .ros_executable import RosExecutable
1821
from ..parameter_descriptions import Parameter
1922
from ..parameter_descriptions import ParameterFile
2023
from ..parameter_descriptions import ParameterValue
2124

2225

2326
__all__ = [
2427
'ComposableNode',
28+
'Node',
29+
'NodeTrait',
2530
'Parameter',
2631
'ParameterFile',
2732
'ParameterValue',
33+
'RosExecutable',
2834
]

0 commit comments

Comments
 (0)