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.
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.
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.
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.