Skip to content

Commit

Permalink
command line args, rosparams, and environment vars
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Apr 30, 2012
1 parent a2ec555 commit e488dd9
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 43 deletions.
32 changes: 29 additions & 3 deletions html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
<script src="bootstrap/js/bootstrap.min.js"></script>
<style>
body { overflow: hidden; }
.modal { display: none; top: 200px; margin-top: 0; }
.modal { display: none; top: 200px; margin-top: 0; width: 530px; }
#terminal_output_data { background: black; color: white; border: none; }
#launch_settings textarea, #launch_settings input { width: 490px; }
.navbar .container { width: 100%; }
.navbar .brand { margin-left: -10px; }
.GraphBox .title { padding-right: 28px; }
Expand Down Expand Up @@ -48,6 +49,31 @@ <h3>Terminal Output</h3>
</div>
</div>

<div id="launch_settings" class="modal">
<div class="modal-header">
<a class="close" data-dismiss="modal">&times;</a>
<h3>Launch Settings</h3>
</div>
<div class="modal-body">
<p>These settings will take effect when the node is next started.</p>
<form onsubmit="javascript:ui.setLaunchSettings(true);return false">
<input id="launch_settings_node_name" type="hidden">
<p><b>Command-line arguments</b></p>
<input id="launch_settings_cmd_line_args" placeholder='--flag --key="value with spaces"'>
<!-- HACK: Placeholders can't have newlines, but browsers will wrap text after a lot of spaces -->
<p><b>ROS params</b> (key = value, one per line, JSON syntax)</p>
<textarea id="launch_settings_rosparams" placeholder="param1 = 0 /namespace/param2 = { a: 1, b: 2 }"></textarea>
<p><b>Environment variables</b> (key = value, one per line)</p>
<textarea id="launch_settings_env_vars" placeholder="VAR1 = 0 VAR2 = text with spaces"></textarea>
</form>
</div>
<div class="modal-footer">
<a class="btn" data-dismiss="modal">Cancel</a>
<a href="javascript:ui.setLaunchSettings(false)" class="btn">Change</a>
<a href="javascript:ui.setLaunchSettings(true)" class="btn btn-primary">Change and Relaunch</a>
</div>
</div>

<div id="change_connection_url" class="modal">
<div class="modal-header">
<a class="close" data-dismiss="modal">&times;</a>
Expand All @@ -62,13 +88,13 @@ <h3>Change Connection URL</h3>
<form class="form-horizontal" onsubmit="javascript:ui.setConnectionURL();return false">
<div class="control-group">
<label class="control-label" for="new_connection_url">Connection URL</label>
<div class="controls"><input type="text" class="input-xlarge" id="new_connection_url"></div>
<div class="controls"><input type="text" class="input-xlarge" id="new_connection_url" placeholder="ws://localhost:9000"></div>
</div>
</form>
</div>
<div class="modal-footer">
<a class="btn" data-dismiss="modal">Cancel</a>
<a href="javascript:ui.setConnectionURL()" class="btn btn-primary">Change URL</a>
<a href="javascript:ui.setConnectionURL()" class="btn btn-primary">Change</a>
</div>
</div>

Expand Down
62 changes: 41 additions & 21 deletions html/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,28 +137,17 @@ var ride = {
case 'update_owned_node':
var node = this.graph.node(data.name);
if (!node) break;
switch (data.status) {
case STATUS_STARTING:
node.detailText = 'Starting...';
break;
case STATUS_STARTED:
node.detailText = '';
break;
case STATUS_STOPPING:
node.detailText = 'Stopping...';
break;
case STATUS_STOPPED:
node.detailText = (data.return_code === null) ? '' : 'Exited with code ' + data.return_code;
break;
case STATUS_ERROR:
node.detailText = 'Could not be launched (run rosmake?)';
break;
}
node.isRunning = data.is_running;
node.detailText = data.status;
node.updateHTML();
$(node.startElement).toggle(data.status == STATUS_STOPPED || data.status == STATUS_ERROR);
$(node.stopElement).toggle(data.status != STATUS_STOPPED && data.status != STATUS_ERROR);
$(node.startElement).toggle(!data.is_running);
$(node.stopElement).toggle(data.is_running);
this.graph.updateBounds();
this.graph.draw();
if (!data.is_running && node.relaunchWhenStopped) {
node.relaunchWhenStopped = false;
ROS.call('/ride/node/start', { name: data.name });
}
break;
}
},
Expand Down Expand Up @@ -258,6 +247,17 @@ var ui = {
});
$(node.stopElement).show();

// Launch settings
item('Launch settings', function() {
ROS.call('/ride/node/settings/get', { name: node.name }, function(data) {
$('#launch_settings_node_name').val(node.name);
$('#launch_settings_cmd_line_args').val(data.cmd_line_args);
$('#launch_settings_rosparams').val(data.rosparams);
$('#launch_settings_env_vars').val(data.env_vars);
$('#launch_settings').modal('show');
});
});

// Show terminal output
item('Show terminal output', function() {
ROS.call('/ride/node/output', { name: node.name }, function(data) {
Expand All @@ -279,6 +279,26 @@ var ui = {
$('#change_connection_url').modal('hide');
},

setLaunchSettings: function(relaunch) {
var node_name = $('#launch_settings_node_name').val();
ROS.call('/ride/node/settings/set', {
name: node_name,
cmd_line_args: $('#launch_settings_cmd_line_args').val(),
rosparams: $('#launch_settings_rosparams').val(),
env_vars: $('#launch_settings_env_vars').val()
}, function() {
var node = ride.graph.node(node_name);
if (!node) return;
if (node.isRunning) {
ROS.call('/ride/node/stop', { name: node_name });
node.relaunchWhenStopped = true;
} else {
ROS.call('/ride/node/start', { name: node_name });
}
});
$('#launch_settings').modal('hide');
},

setConnected: function(connected) {
if (connected) {
$('#connection_status').text('Connected to ' + ROS.url);
Expand Down Expand Up @@ -331,8 +351,8 @@ ride.graph.draw();

// Compute which input has focus
var inputWithFocus = null;
$('input').focus(function() { inputWithFocus = this; });
$('input').blur(function() { inputWithFocus = null; });
$('input, textarea').focus(function() { inputWithFocus = this; });
$('input, textarea').blur(function() { inputWithFocus = null; });

// Deselect input when the graph is clicked
$(ride.graph.element).click(function() {
Expand Down
98 changes: 79 additions & 19 deletions nodes/ride.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
import json
import fcntl
import shlex
import rospy
import signal
import pickle
Expand All @@ -13,12 +14,6 @@
from ros import rosnode
from std_msgs.msg import String

STATUS_STARTING = 0
STATUS_STARTED = 1
STATUS_STOPPING = 2
STATUS_STOPPED = 3
STATUS_ERROR = 4

TOPIC_NAMES_TO_IGNORE = ['/rosout']
NODE_NAMES_TO_IGNORE = ['/rosout', '/rosbridge', '/ride']

Expand Down Expand Up @@ -98,10 +93,12 @@ def __init__(self, ride, name, path, display_name):
self.path = path
self.display_name = display_name
self.stdout = ''
self.status = STATUS_STARTING
self.return_code = None
self.status = 'Not running'
self.process = None
self.remappings = {}
self.cmd_line_args = ''
self.rosparams = ''
self.env_vars = ''
self.ride.updates.create_node(self)
self.ride.names_to_avoid.add(name)
self.start()
Expand Down Expand Up @@ -170,20 +167,65 @@ def start(self):
for topic in map:
command.append(topic + ':=' + map[topic])

# Start the node again
# Prepare for an early return
self.stdout = '$ ' + self.path + '\n'
self.status = 'Failed to launch'

# Shared parser logic for multiline key=value pairs
def parse_multiline(text):
for line in text.split('\n'):
line = line.strip()
if not line:
continue
if '=' not in line:
raise Exception('Line missing "="')
key, value = line.split('=', 1)
yield key.strip(), value.strip()

# Attempt to append command-line arguments
try:
command += shlex.split(self.cmd_line_args)
self.stdout = '$ ' + self.path + ' ' + self.cmd_line_args + '\n'
except Exception as e:
self.status = 'Failed to launch: Could not parse command-line arguments (%s)' % str(e)
self.ride.updates.update_owned_node(self)
return

# Attempt to parse environment variables
env_vars = dict(os.environ)
try:
for key, value in parse_multiline(self.env_vars):
env_vars[key] = value
except Exception as e:
self.status = 'Failed to launch: Could not parse environment variables (%s)' % str(e)
self.ride.updates.update_owned_node(self)
return

# Attempt to parse rosparams
try:
for key, value in parse_multiline(self.rosparams):
if key and '/' not in key:
key = self.name + '/' + key # Handle private names
value = json.loads('{ "value": %s }' % value)['value']
rospy.set_param(key, value)
except Exception as e:
self.status = 'Failed to launch: Could not parse ROS params (%s)' % str(e)
self.ride.updates.update_owned_node(self)
return

# Start the node again
try:
# Start the node as a child process
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
self.status = STATUS_STARTING
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env_vars)
self.status = 'Starting...'

# Make sure self.process.stdout.read() won't block
f = self.process.stdout
fcntl.fcntl(f, fcntl.F_SETFL, fcntl.fcntl(f, fcntl.F_GETFL) | os.O_NONBLOCK)
except Exception as e:
# This will fail if the file doesn't exist (need to use rosmake again)
self.stdout += str(e)
self.status = STATUS_ERROR
self.status = 'Failed to launch: popen() failed (run rosmake?)'

# Send the new status
self.ride.updates.update_owned_node(self)
Expand All @@ -192,11 +234,11 @@ def stop(self):
if self.process:
# Don't use self.ride.soft_kill_process(self.process) because we
# are still checking for the return code in self.poll()
if self.status == STATUS_STOPPING:
if self.status == 'Stopping...':
self.process.kill()
else:
self.process.send_signal(signal.SIGINT)
self.status = STATUS_STOPPING
self.status = 'Stopping...'
self.ride.updates.update_owned_node(self)

def poll(self):
Expand All @@ -207,8 +249,7 @@ def poll(self):
except:
pass
if self.process.returncode is not None:
self.status = STATUS_STOPPED
self.return_code = self.process.returncode
self.status = 'Exited with return code %d' % self.process.returncode
self.process = None
self.ride.updates.update_owned_node(self)

Expand Down Expand Up @@ -293,7 +334,7 @@ def update_owned_node(self, node, send=True):
'type': 'update_owned_node',
'name': node.name,
'status': node.status,
'return_code': node.return_code,
'is_running': node.status in ['Starting...', '', 'Stopping...'],
}, send)

class RIDE:
Expand Down Expand Up @@ -326,6 +367,8 @@ def __init__(self):
rospy.Service('/ride/node/start', ride.srv.NodeStart, self.node_start_service)
rospy.Service('/ride/node/stop', ride.srv.NodeStop, self.node_stop_service)
rospy.Service('/ride/node/output', ride.srv.NodeOutput, self.node_output_service)
rospy.Service('/ride/node/settings/get', ride.srv.NodeSettingsGet, self.node_settings_get_service)
rospy.Service('/ride/node/settings/set', ride.srv.NodeSettingsSet, self.node_settings_set_service)
rospy.Service('/ride/link/create', ride.srv.LinkCreate, self.link_create_service)
rospy.Service('/ride/link/destroy', ride.srv.LinkDestroy, self.link_destroy_service)

Expand Down Expand Up @@ -413,6 +456,23 @@ def node_output_service(self, request):
return ride.srv.NodeOutputResponse(self.owned_nodes[request.name].stdout, True)
return ride.srv.NodeOutputResponse('', False)

def node_settings_get_service(self, request):
'''Implements the /ride/node/settings/get service'''
if request.name in self.owned_nodes:
node = self.owned_nodes[request.name]
return ride.srv.NodeSettingsGetResponse(node.cmd_line_args, node.rosparams, node.env_vars, True)
return ride.srv.NodeSettingsGetResponse('', '', '', False)

def node_settings_set_service(self, request):
'''Implements the /ride/node/settings/set service'''
if request.name in self.owned_nodes:
node = self.owned_nodes[request.name]
node.cmd_line_args = request.cmd_line_args
node.rosparams = request.rosparams
node.env_vars = request.env_vars
return ride.srv.NodeSettingsSetResponse(True)
return ride.srv.NodeSettingsSetResponse(False)

def link_create_service(self, request):
'''Implements the /ride/link/create service'''
key = request.from_topic, request.to_topic
Expand Down Expand Up @@ -504,8 +564,8 @@ def poll(self):
node = self.owned_nodes[name]

# Check on the process
if node.status == STATUS_STARTING and node.name in node_names:
node.status = STATUS_STARTED
if node.status == 'Starting...' and node.name in node_names:
node.status = ''
self.updates.update_owned_node(node)
node.poll()

Expand Down
6 changes: 6 additions & 0 deletions srv/NodeSettingsGet.srv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
string name
----
string cmd_line_args
string rosparams
string env_vars
bool worked
6 changes: 6 additions & 0 deletions srv/NodeSettingsSet.srv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
string name
string cmd_line_args
string rosparams
string env_vars
----
bool worked

0 comments on commit e488dd9

Please sign in to comment.