In this tutorial, we will create a Python script that launches ROS with the help of the subprocess module. Code used in this tutorial can be found on my bitbucket under the directory start_ros_with_py.

All below commands are run on a Google Cloud Ubuntu 18.04 VM with 4 cores and 15 GB Ram (n1-standard-4 ). Because such a VM is not set up with the environment needed in this tutorial, I prepared a shell script which installs docker, docker-compose and sets up a container from the previous post, except with TensorFlow CPU instead of GPU. The shell script can be found here, and can be executed like so:

# clone repo
$ git clone https://reinka@bitbucket.org/reinka/blog.git 
# change to the directory containing the setup script
$ cd blog/start_ros_with_py
# execute setup script
$ . setup.sh

Once inside the container build the workspace and source the setup.bash:

$ cd /notebooks/workspace
$ catkin_make
$ . devel/setup.bash

Our workspace is ready. Now we can automize our ROS launch.

Helper script for starting a talker node

For the sake of demonstration (we could do this without a script in our case, however for more complex applications you might want to use scripts), we will create a shell script talker.sh that starts the talker node:

/opt/ros/kinetic/bin/rosrun tutorial talker.py

Save it under /notebooks/workspace/talker.sh and make it executable via chmod +x talker.sh.

Python Script for starting roscore and a talker node as subprocesses

Finally we create a Python script that starts two subprocesses: roscore and the talker node from the previous post by using the talker.sh shell script we just created. Save it under /notebooks/workspace/start_ros.py

import os
import signal
from subprocess import Popen
import time


def run(cmd, stdout, stderr):
    """Run a given `cmd` in a subprocess, write logs to stdout / stderr.

    Parameters
    ----------
    cmd : list of str
        Command to run.
    stdout : str or subprocess.PIPE object
        Destination of stdout output.
    stderr : str or subprocess.PIPE object
        Destination of stderr output.

    Returns
    -------
    A subprocess.Popen instance.
    """
    return Popen(cmd, stdout=stdout, stderr=stderr, shell=False,
                 preexec_fn=os.setsid)


def get_stdout_stderr(typ, datetime, dir):
    """Create stdout / stderr file paths."""
    out = '%s_%s_stdout.log' % (datetime, typ)
    err = '%s_%s_stderr.log' % (datetime, typ)
    return os.path.join(dir, out), os.path.join(dir, err)


def check_files_exist(files):
    """Check if given list of files exists.

    Parameters
    ----------
    files : list of str
        Files to check for existence.

    Returns
    -------
    None if all files exist. Else raises a ValueError.
    """
    errors = []
    for f in files:
        if not os.path.exists(f):
            errors.append(f)
    if errors:
        raise ValueError('File does not exist: %s' % errors)


def start_process(cmd, typ, start_time, dpath_logs):
    """Start a subprocess with the given command `cmd`.

    Parameters
    ----------
    cmd : list of str
        Command to run.
    typ : str
        Type of subprocess. This will be included in the logs' file names.
    start_time : str
        Datetime string, will be included in the logs' file names as well as
        the resulting bag's name.
    dpath_logs :
        Path to log direcotry.

    Returns
    -------
    A subprocess.Popen instance.
    """
    print('Starting', typ.upper())
    stdout, stderr = get_stdout_stderr(typ, start_time, dpath_logs)
    with open(stdout, 'wb') as out, open(stderr, 'wb') as err:
        return run(cmd, stdout=out, stderr=err)


def main(args):
    dpath_logs = args.dpath_logs
    script_node = args.script_node
    check_files_exist([script_node, dpath_logs])

    start_time = time.strftime('%Y%m%d_%H%M%S')

    p_ros_core = start_process(['/opt/ros/kinetic/bin/roscore'],
                                'ros', start_time, dpath_logs)

    session_talker_node = start_process(['/bin/bash', script_node],
                               'talker_node', start_time,
                               dpath_logs)

    # print pids in case something goes wrong
    print('PGID ROS: ', os.getpgid(p_ros_core.pid))
    print('PGID TALKER NODE: ', os.getpgid(session_talker_node.pid))


    time.sleep(5)
    print('Killing ROS and talker node.')
    os.killpg(os.getpgid(p_ros_core.pid), signal.SIGTERM)
    os.killpg(os.getpgid(session_talker_node.pid), signal.SIGTERM)


if __name__ == '__main__':
    """Start ROS and the talker node each as a subprocess.

    Examples
    --------

    python  start_ros.py --script_node /notebooks/workspace/talker.sh \
    -l /notebooks/workspace/src/scripts
    """
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('--script_node', type=str,
                        default='/notebooks/workspace/talker.sh')
    parser.add_argument('--dpath_logs', '-l', type=str,
                        default='/notebooks/workspace/src/scripts')
    args = parser.parse_args()
    main(args)

The important parts here are the main and run functions.

The main function first checks if the provided paths exist. It then creates a timestamp which will be used for the names of the log files of the spawned subprocesses. Each subprocess gets started via start_process which calls run: it uses Popen to create a subprocess, we specify stdout and stderr for debugging reasons, and set the session ID through os.setsid().

After 5 seconds, both processes get killed via a SIGTERM signal.

The script can be finally run via:

root@d70ea6b805fb:/notebooks/workspace $ python start_ros.py 
Starting ROS
Starting CM
PGID ROS:  674
PGID CM:  675
Killing ROS and talker node.

We can now check the output of our talker node:

root@d70ea6b805fb:/notebooks/workspace $ ll src/scripts/
total 24
drwxrwxr-x 2 1001 1002 4096 Apr  6 21:48 ./
drwxr-xr-x 4 root root 4096 Apr  6 20:32 ../
-rw-r--r-- 1 root root    0 Apr  6 21:48 20190406_214805_ros_stderr.log
-rw-r--r-- 1 root root  877 Apr  6 21:48 20190406_214805_ros_stdout.log
-rw-r--r-- 1 root root 1348 Apr  6 21:48 20190406_214805_talker_node_stderr.log
-rw-r--r-- 1 root root 1793 Apr  6 21:48 20190406_214805_talker_node_stdout.log
-rwxrwxr-x 1 1001 1002  652 Apr  6 20:18 talker.py*

root@d70ea6b805fb:/notebooks/workspace# tail -n 3 src/scripts/20190406_214805_talker_node_stdout.log
[INFO] [1554587290.251599]: hello world [-2.2965705]
[INFO] [1554587290.351432]: hello world [1.0656843]
[INFO] [1554587290.451944]: hello world [-0.06105766]

That's it.

Instead of starting roscore and rosrun each as its own subprocess, we could use one single script which e.g. uses roslaunch and launchfiles to start the whole ROS stack, and move it to one subprocess. In case you work with additional software that interacts with ROS, e.g. a simulation, it could be started in a second process, etc.

Next steps

In the example above, ROS runs for a (hardcoded) period of time. In the next blog post, we will learn how to interact with our Python script start_ros.py from within our talker node in order to send a kill signal from within that node. This is usefull if you need to stop your ROS application after a certain state is reached inside your node, e.g. in case of training a reinforcement learning agent after a certain number of iterations / environment interactions.

Getagged mit:
ROS Python3
blog comments powered by Disqus