diff --git a/CMakeLists.txt b/CMakeLists.txt index cfba2ac..bb75223 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 2.8.3) +cmake_minimum_required(VERSION 3.0.2) project(roslaunch2) find_package(catkin REQUIRED) diff --git a/README.md b/README.md index cdf9509..8330dec 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ * [License & Citing](#cite) # Overview -roslaunch2 is a (pure Python based) ROS package that facilitates writing **versatile, flexible and dynamic launch configurations for the Robot Operating System (ROS 1) using Python**, both for simulation and real hardware setups, as contrasted with the existing XML based launch file system of ROS, namely [roslaunch](http://wiki.ros.org/roslaunch). Note that roslaunch2 is not (yet) designed and developed for ROS 2 but for ROS 1 only although it may also inspire the development (of the launch system) of ROS 2. It is **compatible with all ROS versions providing roslaunch** which is used as its backend. roslaunch2 has been tested and heavily used on ROS Indigo, Jade, Kinetic, and Lunar; it also supports a “dry-mode” to generate launch files without ROS being installed at all. The **key features** of roslaunch2 are +roslaunch2 is a (pure Python based) ROS package that facilitates writing **versatile, flexible and dynamic launch configurations for the Robot Operating System (ROS 1) using Python**, both for simulation and real hardware setups, as contrasted with the existing XML based launch file system of ROS, namely [roslaunch](http://wiki.ros.org/roslaunch). Note that roslaunch2 is not (yet) designed and developed for ROS 2 but for ROS 1 only although it may also inspire the development (of the launch system) of ROS 2. It is **compatible with all ROS versions providing roslaunch** which is used as its backend. roslaunch2 has been tested and used on ROS Indigo, Jade, Kinetic, Lunar, Melodic, and Noetic; it also supports a “dry-mode” to generate launch files without ROS being installed at all. The **key features** of roslaunch2 are - versatile control structures (conditionals, loops), - extended support for launching and querying information remotely, - an easy-to-use API for also launching from Python based ROS nodes dynamically, as well as @@ -106,6 +106,6 @@ The entire code is **BSD 3-Clause licenced**, see [here](https://github.com/Code } ``` -Copyright (c) 2017-2019, Adrian Böckenkamp, Department of Computer Science VII, TU Dortmund University. +Copyright (c) 2017-2020, Adrian Böckenkamp, Department of Computer Science VII, TU Dortmund University. All rights reserved. diff --git a/package.xml b/package.xml index 6d514df..fe30775 100644 --- a/package.xml +++ b/package.xml @@ -1,5 +1,5 @@ - + roslaunch2 0.0.1 @@ -18,9 +18,12 @@ catkin python-catkin-pkg + python3-setuptools + python-setuptools python-enum34-pip pyro4 python-rospkg roslaunch + rosbash diff --git a/setup.py b/setup.py index f6a23b6..83da1d8 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from distutils.core import setup +from setuptools import setup from catkin_pkg.python_setup import generate_distutils_setup d = generate_distutils_setup( diff --git a/src/roslaunch2/__init__.py b/src/roslaunch2/__init__.py index 4356cd5..8386089 100644 --- a/src/roslaunch2/__init__.py +++ b/src/roslaunch2/__init__.py @@ -3,21 +3,21 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 12/11/2019 +# Date: 08/06/2020 # Import all submodules typically used in launch modules: -from group import * -from parameter import * -from machine import * -from package import * -from logger import * -from utils import * -from remote import * -from launch import * -from node import * -from environment import * -from test import * -from helpers import * +from .group import * +from .parameter import * +from .machine import * +from .package import * +from .logger import * +from .utils import * +from .remote import * +from .launch import * +from .node import * +from .environment import * +from .test import * +from .helpers import * import argparse @@ -172,7 +172,8 @@ def terminate(instance): def main(command_line_args=None): """ Defines the core logic (= Python based dynamic launch files) of roslaunch2. It does NOT create any - launch modules or the like. + launch modules or the like. This function is not meant to be called directly. See `start()` and + `start_async()` for more details. :param command_line_args: List with command line arguments as strings :return: None diff --git a/src/roslaunch2/environment.py b/src/roslaunch2/environment.py index 54719db..aa23b19 100644 --- a/src/roslaunch2/environment.py +++ b/src/roslaunch2/environment.py @@ -3,13 +3,13 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 13/03/2018 +# Date: 08/06/2020 import lxml.etree import warnings -import interfaces -import machine +from . import interfaces +from . import machine class EnvironmentVariable(interfaces.GeneratorBase, interfaces.Composable): diff --git a/src/roslaunch2/group.py b/src/roslaunch2/group.py index 3797678..c30b3dc 100644 --- a/src/roslaunch2/group.py +++ b/src/roslaunch2/group.py @@ -3,15 +3,15 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 13/03/2018 +# Date: 08/06/2020 import lxml.etree import warnings -import interfaces -import remapable -import node -import launch +from . import interfaces +from . import remapable +from . import node +from . import launch class Group(remapable.Remapable, interfaces.Composer, interfaces.Composable): diff --git a/src/roslaunch2/helpers.py b/src/roslaunch2/helpers.py index 1c005b6..8db2556 100644 --- a/src/roslaunch2/helpers.py +++ b/src/roslaunch2/helpers.py @@ -3,10 +3,10 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 12/11/2019 +# Date: 08/06/2020 -import logger -import node +from . import logger +from . import node class Helpers: diff --git a/src/roslaunch2/interfaces.py b/src/roslaunch2/interfaces.py index cf71ff5..9a553ea 100644 --- a/src/roslaunch2/interfaces.py +++ b/src/roslaunch2/interfaces.py @@ -3,7 +3,7 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 26/01/2018 +# Date: 08/06/2020 import copy import lxml.etree @@ -126,9 +126,9 @@ def add_env_variables_to_nodes(self, environment_variable_dict=None): """ if environment_variable_dict is None: environment_variable_dict = {} - from environment import EnvironmentVariable - from group import Group - from node import Node + from .environment import EnvironmentVariable + from .group import Group + from .node import Node # Copy dict to not change the argument in higher recursion levels: tmp_env_dict = dict(environment_variable_dict) diff --git a/src/roslaunch2/launch.py b/src/roslaunch2/launch.py index 8406c49..2ba41e6 100644 --- a/src/roslaunch2/launch.py +++ b/src/roslaunch2/launch.py @@ -3,16 +3,16 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 13/03/2018 +# Date: 08/06/2020 import warnings import lxml.etree -import interfaces -import machine -import remapable -import node -import group +from . import interfaces +from . import machine +from . import remapable +from . import node +from . import group class Launch(interfaces.Composable, interfaces.Composer, remapable.Remapable): diff --git a/src/roslaunch2/machine.py b/src/roslaunch2/machine.py index 985da49..b160e83 100644 --- a/src/roslaunch2/machine.py +++ b/src/roslaunch2/machine.py @@ -3,7 +3,7 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 13/03/2018 +# Date: 08/06/2020 import lxml.etree import getpass @@ -12,9 +12,9 @@ import socket import enum -import interfaces -import utils -import remote +from . import interfaces +from . import utils +from . import remote class Machine(interfaces.GeneratorBase): diff --git a/src/roslaunch2/node.py b/src/roslaunch2/node.py index 62c320f..4e68310 100644 --- a/src/roslaunch2/node.py +++ b/src/roslaunch2/node.py @@ -3,21 +3,21 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 13/03/2018 +# Date: 08/06/2020 import warnings import lxml.etree import enum - -import remapable -import interfaces -import package -import machine -import parameter -import environment import random import string +from . import remapable +from . import interfaces +from . import package +from . import machine +from . import parameter +from . import environment + class Output(enum.IntEnum): """ @@ -151,7 +151,7 @@ def __del__(self): warnings.warn('{} has been created but never add()ed.'.format(str(self)), Warning, 2) def __str__(self): - return '{:s}@{:s}: {:s}'.format(self.node, self.pkg, self.name) + return '{:s}@{:s}: {:s}'.format(str(self.node), str(self.pkg), str(self.name)) def add(self, param): """ diff --git a/src/roslaunch2/package.py b/src/roslaunch2/package.py index 1c666dc..f723e8b 100644 --- a/src/roslaunch2/package.py +++ b/src/roslaunch2/package.py @@ -3,14 +3,14 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 13/03/2018 +# Date: 08/06/2020 import rospkg import os import sys import Pyro4 -import logger +from . import logger class Package: @@ -79,9 +79,8 @@ def __init__(self, name=None, silent=False): :param name: Name of ROS package :param silent: True if no exceptions should be thrown if the package was not found """ - self.name = name try: - self.path = Package.__get_pkg_path_cached(name) + self.set_name(name) except rospkg.ResourceNotFound: if not silent: raise @@ -93,7 +92,7 @@ def get_name(self): :return: ROS package name """ - return self.name + return self.__name @Pyro4.expose def set_name(self, name): @@ -102,8 +101,8 @@ def set_name(self, name): :param name: ROS package name """ - self.name = name - self.path = Package.__get_pkg_path_cached(name) + self.__name = name + self.__path = Package.__get_pkg_path_cached(name) name = property(get_name, set_name) @@ -114,16 +113,16 @@ def get_path(self): :return: ROS package path """ - return self.path + return self.__path def _set_path(self, pkg_path): # not exposed to Pyro! - if self.name: - self.path = pkg_path + if self.__name: + self.__path = pkg_path else: - self.path = None + self.__path = None def __nonzero__(self): - return bool(self.path) # for Python 2.x + return bool(self.__path) # for Python 2.x def __bool__(self): return self.__nonzero__() # for Python 3.x @@ -131,7 +130,7 @@ def __bool__(self): path = property(get_path, _set_path) def __str__(self): - return self.name + return self.__name @staticmethod def valid(pkg): @@ -162,7 +161,7 @@ def has_node(self, node_name, warn=True): :param warn: True if a warning about the missing node should be emitted :return: True if node exists, False otherwise """ - pkg = os.path.join(self.path, '../..') + pkg = os.path.join(self.__path, '../..') # Just consider files that are executable: if [f for f in Package.get_paths_to_file(pkg, node_name) if os.access(f, os.X_OK)]: # if len(res) > 1: @@ -171,7 +170,7 @@ def has_node(self, node_name, warn=True): return True else: if warn: - logger.warning("Node '{}' in package '{}' not found.".format(node_name, self.name)) + logger.warning("Node '{}' in package '{}' not found.".format(node_name, self.__name)) return False @staticmethod @@ -201,7 +200,7 @@ def use(self, path_comp, **kwargs): path_comp += '.pyl' mod_path = self.find(path_comp, True) if not mod_path: - raise ValueError("Launch module '{:s}' in package '{:s}' not found.".format(path_comp, self.name)) + raise ValueError("Launch module '{:s}' in package '{:s}' not found.".format(path_comp, self.__name)) m = Package.import_launch_module(mod_path) return m.main(**kwargs) @@ -223,7 +222,7 @@ def import_launch_module(full_module_path): search_path = os.path.dirname(os.path.abspath(module_name)) if search_path not in sys.path: sys.path.append(search_path) - if sys.version_info < (3, 3): # Python 2.x and 3.x where x < 3 + if sys.version_info < (3, 3): # Python 2.x and 3.y where x >= 4 and y < 3 import imp return imp.load_source(module_name, full_module_path) elif sys.version_info < (3, 4): # Python 3.3 and 3.4 @@ -231,6 +230,9 @@ def import_launch_module(full_module_path): return importlib.machinery.SourceFileLoader(module_name, full_module_path).load_module() elif sys.version_info >= (3, 5): # Python 3.5+ import importlib.util + import importlib.machinery + # Allow any extenstions (not only .py and .so, and .pyl in particular): + importlib.machinery.SOURCE_SUFFIXES.append('') spec = importlib.util.spec_from_file_location(module_name, full_module_path) m = importlib.util.module_from_spec(spec) spec.loader.exec_module(m) @@ -246,21 +248,21 @@ def find(self, path_comp, silent=False): case of failure :return: first found file (full path) or None if silent==True and nothing found """ - key = ''.join([self.name, path_comp]) + key = ''.join([self.__name, path_comp]) if key in Package.__find_cache: return Package.__find_cache[key] if not path_comp: - return self.path - dir_path = os.path.join(self.path, path_comp if not path_comp.startswith(os.path.sep) else path_comp[1:]) + return self.__path + dir_path = os.path.join(self.__path, path_comp if not path_comp.startswith(os.path.sep) else path_comp[1:]) if os.path.isdir(dir_path): Package.__find_cache[key] = dir_path return dir_path - f = Package.get_paths_to_file(self.path, path_comp) + f = Package.get_paths_to_file(self.__path, path_comp) if len(f) > 1: logger.log("Found {} files, unique selection impossible (using first).".format(', '.join(f))) if not f: if not silent: - raise IOError("No files like '{}' found in '{}'.".format(path_comp, self.name)) + raise IOError("No files like '{}' found in '{}'.".format(path_comp, self.__name)) else: return None Package.__find_cache[key] = f[0] @@ -285,6 +287,6 @@ def selective_find(self, path_comp_options, path_comp_prefix='', silent=False): return path # Nothing found if not silent: - raise IOError("None of the queried files found in '{}'.".format(self.name)) + raise IOError("None of the queried files found in '{}'.".format(self.__name)) else: return None diff --git a/src/roslaunch2/parameter.py b/src/roslaunch2/parameter.py index ff689ed..268c32e 100644 --- a/src/roslaunch2/parameter.py +++ b/src/roslaunch2/parameter.py @@ -3,19 +3,19 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 13/03/2018 +# Date: 08/06/2020 import lxml.etree import warnings import argparse import os.path import yaml - -import interfaces -import machine -import logger import enum +from . import interfaces +from . import machine +from . import logger + def load_from_file(path, only_parse_known_args): """ @@ -35,7 +35,7 @@ def __call__(self, parser, namespace, values, option_string=None): filename = "{:s}.yaml".format(values[0]) filepath = os.path.join(path, filename) try: - f = yaml.load(file(filepath, 'r')) + f = yaml.safe_load(file(filepath, 'r')) for key, value in f.iteritems(): if value is not None: if only_parse_known_args: @@ -52,17 +52,24 @@ class LaunchParameter(argparse.ArgumentParser): """ Represents a parameter for a launch module. For example, this can influence whether to select simulator A or B. These parameters are NOT consumed by ROS nodes (refer to ``ServerParameter`` and ``FileParameter`` in such cases). + Such parameters are passed by command line, for example: roslaunch2 my_pkg my_launch.pyl --param foo + + Whats special about this class is that all command line parameters from all (possibly included) launch modules are + added so that calling roslaunch2 with the special command line flag "--ros-args" prints all available parameters + for the given launch file along with a description of it (like roslaunch does). In order to make this work, you + must finally call 'get_args()'. """ launch_parameter_list = [] # Static list collection all LaunchParameter instances. - def __init__(self, prog=None, description=None, epilog=None, version=None, + def __init__(self, prog=None, description=None, epilog=None, parents=None, formatter_class=argparse.HelpFormatter, prefix_chars='-', fromfile_prefix_chars=None, argument_default=None, conflict_handler='resolve'): if parents is None: parents = [] - argparse.ArgumentParser.__init__(self, prog, str(), description, epilog, version, parents, - formatter_class, prefix_chars, fromfile_prefix_chars, - argument_default, conflict_handler, add_help=False) + argparse.ArgumentParser.__init__(self, prog=prog, usage=None, description=description, epilog=epilog, + parents=parents, formatter_class=formatter_class, prefix_chars=prefix_chars, + fromfile_prefix_chars=fromfile_prefix_chars, argument_default=argument_default, + conflict_handler=conflict_handler, add_help=False) self.ros_argument_group = self.add_argument_group(title='ROS launch module arguments', description=None) def add(self, name, help_text, default, short_name=None, **kwargs): diff --git a/src/roslaunch2/remapable.py b/src/roslaunch2/remapable.py index d241154..3716b29 100644 --- a/src/roslaunch2/remapable.py +++ b/src/roslaunch2/remapable.py @@ -3,11 +3,11 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 13/03/2018 +# Date: 08/06/2020 from lxml import etree -import interfaces +from . import interfaces class Remapable(interfaces.GeneratorBase): diff --git a/src/roslaunch2/remote.py b/src/roslaunch2/remote.py index b826453..0cb3f23 100644 --- a/src/roslaunch2/remote.py +++ b/src/roslaunch2/remote.py @@ -3,14 +3,14 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 13/03/2018 +# Date: 08/06/2020 import os import Pyro4 -import utils import tempfile -import package +from . import utils +from . import package __all__ = ["API", "Resolvable", "Path", "Variable"] diff --git a/src/roslaunch2/test.py b/src/roslaunch2/test.py index cc88b1b..67790fc 100644 --- a/src/roslaunch2/test.py +++ b/src/roslaunch2/test.py @@ -3,10 +3,10 @@ # # Author: Adrian Böckenkamp # License: BSD (https://opensource.org/licenses/BSD-3-Clause) -# Date: 13/03/2018 +# Date: 08/06/2020 -import interfaces -import node +from . import interfaces +from . import node class Test(node.Runnable):