Compare commits

...

4 Commits

Author SHA1 Message Date
Oleg Kalachev
c254691f80 Adjust aruco.launch arguments 2020-08-05 02:06:41 +03:00
Oleg Kalachev
4fe6f24d16 Edit readme 2020-07-21 10:09:09 +03:00
Oleg Kalachev
cda7e62dbf Adjust launch file arguments for more convenient configuration 2020-07-21 10:03:40 +03:00
Oleg Kalachev
fa9a1794f6 Add roslaunch_editor 2020-07-21 10:00:33 +03:00
19 changed files with 5484 additions and 24 deletions

View File

@@ -1,7 +1,10 @@
<launch>
<arg name="aruco_detect" default="true"/>
<arg name="aruco_map" default="false"/>
<arg name="aruco_vpe" default="false"/>
<arg name="aruco_detect" default="false"/> <!-- markers recognition enabled -->
<arg name="aruco_map" default="false"/> <!-- markers map recognition enabled -->
<arg name="aruco_vpe" default="false"/> <!-- markers map positioning enabled -->
<arg name="placement" default="floor"/> <!-- markers placement: floor, ceiling, unknown -->
<arg name="length" default="0.33"/> <!-- not-in-map markers length, m -->
<arg name="map" default="map.txt"/> <!-- markers map file name -->
<!-- For additional help go to https://clover.coex.tech/aruco -->
@@ -12,8 +15,9 @@
<remap from="map_markers" to="aruco_map/markers" if="$(arg aruco_map)"/>
<param name="estimate_poses" value="true"/>
<param name="send_tf" value="true"/>
<param name="known_tilt" value="map"/>
<param name="length" value="0.33"/>
<param name="known_tilt" value="map" if="$(eval placement == 'floor')"/>
<param name="known_tilt" value="map_flipped" if="$(eval position == 'ceiling')"/>
<param name="length" value="$(arg length)"/>
<!-- aruco detector parameters -->
<param name="cornerRefinementMethod" value="2"/> <!-- contour refinement -->
<param name="minMarkerPerimeterRate" value="0.075"/> <!-- 0.075 for 320x240, 0.0375 for 640x480 -->
@@ -24,8 +28,9 @@
<remap from="image_raw" to="main_camera/image_raw"/>
<remap from="camera_info" to="main_camera/camera_info"/>
<remap from="markers" to="aruco_detect/markers"/>
<param name="map" value="$(find aruco_pose)/map/map.txt"/>
<param name="known_tilt" value="map"/>
<param name="map" value="$(find aruco_pose)/map/$(arg map)"/>
<param name="known_tilt" value="map" if="$(eval placement == 'floor')"/>
<param name="known_tilt" value="map_flipped" if="$(eval placement == 'ceiling')"/>
<param name="image_axis" value="true"/>
<param name="frame_id" value="aruco_map_detected" if="$(arg aruco_vpe)"/>
<param name="frame_id" value="aruco_map" unless="$(arg aruco_vpe)"/>

View File

@@ -1,16 +1,15 @@
<launch>
<arg name="fcu_conn" default="usb"/>
<arg name="fcu_ip" default="127.0.0.1"/>
<arg name="fcu_sys_id" default="1"/>
<arg name="gcs_bridge" default="tcp"/>
<arg name="web_video_server" default="true"/>
<arg name="rosbridge" default="true"/>
<arg name="main_camera" default="true"/>
<arg name="optical_flow" default="true"/>
<arg name="aruco" default="false"/>
<arg name="rangefinder_vl53l1x" default="true"/>
<arg name="led" default="true"/>
<arg name="rc" default="true"/>
<arg name="fcu_conn" default="usb"/> <!-- FCU connection type: usb, uart, tcp, udp, sitl -->
<arg name="fcu_ip" default="127.0.0.1"/> <!-- FCU IP adddress (if using TCP/UDP) -->
<arg name="fcu_sys_id" default="1"/> <!-- MAVLink system ID, noeditor -->
<arg name="gcs_bridge" default="tcp"/> <!-- GCS bridge type: tcp, udp, udp-b, udp-pb -->
<arg name="web_video_server" default="true"/> <!-- web video server enabled -->
<arg name="rosbridge" default="true"/> <!-- rosbridge_suite enabled, noeditor -->
<arg name="main_camera" default="true"/> <!-- main camera enabled -->
<arg name="optical_flow" default="true"/> <!-- optical flow enabled -->
<arg name="rangefinder_vl53l1x" default="true"/> <!-- VL53l1X rangefinder enabled -->
<arg name="led" default="true"/> <!-- LED strip driver enabled -->
<arg name="rc" default="true"/> <!-- support for mobile RC enabled -->
<arg name="shell" default="true"/>
<!-- log formatting -->
@@ -31,7 +30,7 @@
</node>
<!-- aruco markers -->
<include file="$(find clover)/launch/aruco.launch" if="$(arg aruco)"/>
<include file="$(find clover)/launch/aruco.launch"/>
<!-- optical flow -->
<node pkg="nodelet" type="nodelet" name="optical_flow" args="load clover/optical_flow nodelet_manager" if="$(arg optical_flow)" clear_params="true" output="screen">
@@ -87,4 +86,11 @@
<node pkg="roswww_static" name="roswww_static" type="main.py" clear_params="true">
<param name="default_package" value="clover"/>
</node>
<!-- roslaunch editor parameters -->
<group ns="roslaunch_editor">
<param name="items" type="yaml" value="[clover/clover.launch, clover/main_camera.launch, clover/aruco.launch, clover/led.launch]"/>
<param name="apply_command" value="sudo systemctl restart clover"/>
<param name="hide_uncommented" value="true"/>
</group>
</launch>

View File

@@ -1,7 +1,7 @@
<launch>
<arg name="ws281x" default="true"/>
<arg name="led_effect" default="true"/>
<arg name="led_notify" default="true"/>
<arg name="ws281x" default="true"/> <!-- LED strip driver enabled -->
<arg name="led_effect" default="true"/> <!-- LED effect API enabled -->
<arg name="led_notify" default="all"/> <!-- LED strip notifications: all, battery, none -->
<!-- For additional help go to https://clover.coex.tech/led -->
@@ -22,7 +22,7 @@
<param name="fade_period" value="0.5"/>
<param name="rainbow_period" value="5"/>
<!-- events effects table -->
<rosparam param="notify" if="$(arg led_notify)">
<rosparam param="notify" if="$(eval led_notify == 'all')">
startup: { r: 255, g: 255, b: 255 }
connected: { effect: rainbow }
disconnected: { effect: blink, r: 255, g: 50, b: 50 }
@@ -34,5 +34,8 @@
low_battery: { threshold: 3.7, effect: blink_fast, r: 255, g: 0, b: 0 }
error: { effect: flash, r: 255, g: 0, b: 0 }
</rosparam>
<rosparam param="notify" if="$(eval led_notify == 'battery')">
low_battery: { threshold: 3.7, effect: blink_fast, r: 255, g: 0, b: 0 }
</rosparam>
</node>
</launch>

View File

@@ -0,0 +1,82 @@
cmake_minimum_required(VERSION 2.8.3)
project(roslaunch_editor)
find_package(catkin REQUIRED COMPONENTS message_generation)
add_message_files(
FILES
LaunchFile.msg
)
add_service_files(
FILES
ReadLaunchFiles.srv
WriteLaunchFiles.srv
)
generate_messages(
DEPENDENCIES
# std_msgs # Or other packages containing msgs
)
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES roslaunch_editor
# CATKIN_DEPENDS other_catkin_pkg
# DEPENDS system_lib
)
#############
## Install ##
#############
# all install targets should use catkin DESTINATION variables
# See http://ros.org/doc/api/catkin/html/adv_user_guide/variables.html
## Mark executable scripts (Python etc.) for installation
## in contrast to setup.py, you can choose the destination
# install(PROGRAMS
# scripts/my_python_script
# DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
# )
## Mark executables for installation
## See http://docs.ros.org/melodic/api/catkin/html/howto/format1/building_executables.html
# install(TARGETS ${PROJECT_NAME}_node
# RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
# )
## Mark libraries for installation
## See http://docs.ros.org/melodic/api/catkin/html/howto/format1/building_libraries.html
# install(TARGETS ${PROJECT_NAME}
# ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
# LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
# RUNTIME DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION}
# )
## Mark cpp header files for installation
# install(DIRECTORY include/${PROJECT_NAME}/
# DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}
# FILES_MATCHING PATTERN "*.h"
# PATTERN ".svn" EXCLUDE
# )
## Mark other files for installation (e.g. launch and bag files, etc.)
# install(FILES
# # myfile1
# # myfile2
# DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
# )
#############
## Testing ##
#############
## Add gtest based cpp test target and link libraries
# catkin_add_gtest(${PROJECT_NAME}-test test/test_roslaunch_editor.cpp)
# if(TARGET ${PROJECT_NAME}-test)
# target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME})
# endif()
## Add folders to be run by python nosetests
# catkin_add_nosetests(test)

View File

@@ -0,0 +1,28 @@
# roslaunch_editor
Web-based ROS launch-files editor, created for making configuration of your robot more user-friendly for novices.
<img src="roslaunch_editor.jpg" width=450>
## Quick launch
```bash
roslaunch roslaunch_editor example.launch
```
Then, open `http://localhost:8085/roslaunch_editor/` and edit the test launch file.
## Modes
`roslaunch_editor` works in two modes: standalone mode (where running `editor` node is required), and Clover mode (where `clover`'s `shell` node, `roswww_static` and `rosbridge_suite` are utilized). The editor will read and write launch-files and restart the nodes (if configured) using one of these nodes.
The mode is determined automatically, based on advertised ROS-services.
## Parameters
* `items` (`string` or `list`) launch files to edit, format: `package_name/launch_file_name.launch`.
* `hide_uncommented` (`boolean`, default: `false`)  don't show arguments without comments.
* `apply_command` (`string`, default: `''`) shell command to execute after writing launch-files (e. g. to restart the systemd service).
* `backup` (`boolean`, default: `false`) backup overwritten launch-files (backup is written to `file_name.launch.bak`).
Some parameters (`items`, `hide_uncommented`) can be overwritten over GET-parameters, e. g. `?items=package/foo.launch,package/bar.launch&hide_uncommented=1`.

View File

@@ -0,0 +1,11 @@
<launch>
<include file="$(find roswww)/launch/roswww.launch"/>
<include file="$(find rosbridge_server)/launch/rosbridge_websocket.launch"/>
<node name="roslaunch_editor" pkg="roslaunch_editor" type="editor">
<param name="items" type="yaml" value="[roslaunch_editor/test.launch]"/>
<param name="reference_frames/base_link" value="map"/>
<param name="reference_frames/navigate_target" value="map"/>
</node>
</launch>

View File

@@ -0,0 +1,3 @@
string package
string name
string content

View File

@@ -0,0 +1,45 @@
<?xml version="1.0"?>
<package format="2">
<name>roslaunch_editor</name>
<version>0.0.0</version>
<description>Web based roslaunch files editor</description>
<maintainer email="okalachev@gmail.com">Oleg Kalachev</maintainer>
<license>MIT</license>
<!-- Url tags are optional, but multiple are allowed, one per tag -->
<!-- Optional attribute type can be: website, bugtracker, or repository -->
<url type="repository">https://github.com/CopterExpress/clover</url>
<!-- Author tags are optional, multiple are allowed, one per tag -->
<!-- Authors do not have to be maintainers, but could be -->
<author email="okalachev@gmail.com">Oleg Kalachev</author>
<!-- The *depend tags are used to specify dependencies -->
<!-- Dependencies can be catkin packages or system dependencies -->
<!-- Examples: -->
<!-- Use depend as a shortcut for packages that are both build and exec dependencies -->
<!-- <depend>roscpp</depend> -->
<!-- Note that this is equivalent to the following: -->
<!-- <build_depend>roscpp</build_depend> -->
<!-- <exec_depend>roscpp</exec_depend> -->
<!-- Use build_depend for packages you need at compile time: -->
<!-- <build_depend>message_generation</build_depend> -->
<!-- Use build_export_depend for packages you need in order to build against this package: -->
<!-- <build_export_depend>message_generation</build_export_depend> -->
<!-- Use buildtool_depend for build tool packages: -->
<!-- <buildtool_depend>catkin</buildtool_depend> -->
<!-- Use exec_depend for packages you need at runtime: -->
<!-- <exec_depend>message_runtime</exec_depend> -->
<!-- Use test_depend for packages you need only for testing: -->
<!-- <test_depend>gtest</test_depend> -->
<!-- Use doc_depend for packages you need only for building documentation: -->
<!-- <doc_depend>doxygen</doc_depend> -->
<buildtool_depend>catkin</buildtool_depend>
<depend>message_generation</depend>
<!-- The export tag contains other, unspecified, tags -->
<export>
<!-- Other tools can request additional information be placed here -->
</export>
</package>

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

73
roslaunch_editor/src/editor Executable file
View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python
import rospy
import os
import shutil
import rospkg
from std_srvs.srv import Trigger
from roslaunch_editor.srv import ReadLaunchFiles, ReadLaunchFilesResponse, WriteLaunchFiles
from roslaunch_editor.msg import LaunchFile
rospy.init_node('roslaunch_editor')
rospack = rospkg.RosPack()
backup = rospy.get_param('~backup', True)
apply_command = rospy.get_param('~apply_command', '')
def get_launch_file_path(package, name):
path = rospack.get_path(package)
for root, dirnames, filenames in os.walk(path):
if name in filenames:
return os.path.join(root, name)
raise Exception('Launch file %s/%s not found' % (package, name))
def read(req):
try:
res = ReadLaunchFilesResponse()
for launch_file in req.files:
path = get_launch_file_path(launch_file.package, launch_file.name)
rospy.loginfo('read file %s', path)
launch_file.content = open(path, 'r').read()
res.files.append(launch_file)
res.success = True
return res
except Exception as e:
rospy.logerr(str(e))
return {'success': False, 'message': str(e)}
def write(req):
try:
# write files
for launch_file in req.files:
if not launch_file.name.endswith('.launch'):
raise Exception('Launch file name should end with .launch')
path = get_launch_file_path(launch_file.package, launch_file.name)
rospy.loginfo('write file %s', path)
if backup:
shutil.copyfile(path, path + '.bak.launch')
with open(path, 'w') as f:
f.write(launch_file.content)
# restart the system
if not apply_command:
return {'success': True}
rospy.loginfo('apply: %s', apply_command)
res = os.system(apply_command)
if res == 0:
return {'success': True}
else:
return {'success': False, 'message': 'Error invoking %s' % apply_command}
except Exception as e:
rospy.logerr(str(e))
return {'success': False, 'message': str(e)}
rospy.Service('~read', ReadLaunchFiles, read)
rospy.Service('~write', WriteLaunchFiles, write)
rospy.spin()

View File

@@ -0,0 +1,7 @@
# Read launch-files
LaunchFile[] files # content field is ignored
---
bool success
string message
LaunchFile[] files

View File

@@ -0,0 +1,6 @@
# Write launch-files and apply (restart the system)
LaunchFile[] files
---
bool success
string message

View File

@@ -0,0 +1,34 @@
<launch>
<arg name="string" default="value"/> <!-- simple string value -->
<arg name="no_comment" default="value"/>
<arg name="boolean" default="true"/> <!-- simple boolean -->
<arg name="hidden" default="value"/> <!-- hiddent arg, noeditor -->
<arg name="without_default"/> <!-- without default -->
<arg name="options" default="bar"/> <!-- simple options: foo, bar, baz, killa, gorilla -->
<arg name="options_extra" default="extra"/> <!-- options with extra value: foo, bar, baz -->
<!-- arg with preceding comment -->
<arg name="preceding_comment" default="value"/>
<arg name="dirty_string" default="hello &quot;quotes&quot; and &lt;tags&gt;"/> <!-- "dirty" <arg>'s and comment value -->
<arg name="long_long_long_long_long_arg" default="very long long long long long long long long value"/> <!-- long_long_long_long_long_arg is loooooooooooooooooooooooooooooooooooooooooooooong -->
<arg name="no_subseqent_comment" default="value"/>
<!-- this comment should not be displayed -->
<arg name="integer" default="-245"/> <!-- integer value -->
<arg name="float" default="-0.045"/> <!-- float value -->
<arg name="float_dot" default=".2"/> <!-- float without leading zero -->
<arg name="float_scientific" default="-1e-5"/> <!-- scientific float value -->
<arg name="nan" default="nan"/> <!-- nan value -->
<arg name="inf" default="inf"/> <!-- inf value -->
<arg name="null" default="null"/> <!-- null value -->
</launch>

View File

@@ -0,0 +1,140 @@
<html>
<head>
<script src="roslib.js"></script>
<style>
body {
font-family: sans-serif;
font-size: 14px;
background: white;
padding: 0;
margin: 0;
}
body.connected {
background: white;
}
h1 {
margin-top: 20px;
text-align: center;
}
.editor {
margin: 0px auto;
width: 800px;
overflow: hidden;
text-align: center;
}
.editor label code {
width: 200px;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
white-space: nowrap;
vertical-align: middle;
}
fieldset {
border: 1px solid rgb(232, 232, 232);
padding: 20px;
text-align: left;
margin: 20px 0;
}
.editor label {
display: block;
padding: 10px 0;
-webkit-user-select: none;
user-select: none;
/* margin-bottom: 20px; */
border-bottom: 1px solid rgb(232, 232, 232);
white-space: nowrap;
}
.editor label:last-of-type {
border-bottom: none;
}
.editor label input, select {
font-size: 15px;
font-family: monospace;
width: 200px;
vertical-align: middle;
}
.editor .switch {
width: 200px;
vertical-align: middle;
display: inline-block;
}
.editor .comment {
margin-left: 10px;
vertical-align: middle;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
max-width: 320px;
}
button {
font-size: 20px;
border-radius: 5px;
padding: 10px;
border: none;
cursor: pointer;
margin: 20px;
width: 200px;
background: #4b9dd7;
color: white;
visibility: hidden;
}
body.loaded button {
visibility: visible;
}
textarea {
margin-left: 50px;
font-family: monospace;
vertical-align: top;
font-size: 15px;
white-space: nowrap;
overflow: auto;
font-size: 10px;
position: absolute;
top: 0;
right: 0;
}
.panel {
text-align: center;
position: sticky;
bottom: 0;
width: 100%;
background: rgba(255, 255, 255, 0.9);
}
.loader-overlay {
background: rgba(0,0,0,0.5);
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
}
.loader {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
body.loaded.connected .loader-overlay {
display: none;
}
</style>
<link rel="stylesheet" href="switch.css">
<link rel="stylesheet" href="loader.css">
<title>roslaunch editor</title>
</head>
<body>
<form class="editor">
</form>
<div class="panel">
<button id=applybtn onclick="apply()">Apply</button>
</div>
<div class="loader-overlay">
<div class="loader lds-grid"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
</div>
</body>
<script src="ros.js"></script>
<script src="main.js"></script>
</html>

View File

@@ -0,0 +1,67 @@
.lds-grid {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-grid div {
position: absolute;
width: 15px;
height: 15px;
border-radius: 50%;
background: #fff;
animation: lds-grid 1.2s linear infinite;
}
.lds-grid div:nth-child(1) {
top: 8px;
left: 8px;
animation-delay: 0s;
}
.lds-grid div:nth-child(2) {
top: 8px;
left: 32px;
animation-delay: -0.4s;
}
.lds-grid div:nth-child(3) {
top: 8px;
left: 56px;
animation-delay: -0.8s;
}
.lds-grid div:nth-child(4) {
top: 32px;
left: 8px;
animation-delay: -0.4s;
}
.lds-grid div:nth-child(5) {
top: 32px;
left: 32px;
animation-delay: -0.8s;
}
.lds-grid div:nth-child(6) {
top: 32px;
left: 56px;
animation-delay: -1.2s;
}
.lds-grid div:nth-child(7) {
top: 56px;
left: 8px;
animation-delay: -0.8s;
}
.lds-grid div:nth-child(8) {
top: 56px;
left: 32px;
animation-delay: -1.2s;
}
.lds-grid div:nth-child(9) {
top: 56px;
left: 56px;
animation-delay: -1.6s;
}
@keyframes lds-grid {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}

View File

@@ -0,0 +1,287 @@
var editorElem = document.querySelector('form.editor');
// escape html
function esc(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function getType(value) {
if (value == 'true' || value == 'false') {
return 'bool';
} else if (Number.isInteger(Number(value))) {
return 'int';
} else if (!isNaN(Number(value))) {
return 'float'
} else {
return 'string';
}
}
var items = {};
function generateForm(item, content) {
var parser = new DOMParser();
var doc = items[item].doc = parser.parseFromString(content, 'text/xml');
var html = '';
// go though all arg tags
var args = doc.querySelectorAll('launch > arg');
for (var arg of args) {
var name = arg.getAttribute('name');
var comment = '';
var value = arg.getAttribute('default');
if (value === null) value = '';
var type = getType(value);
// get comment from previous sibling comment with no more than one line break in-between -->
var prev = arg.previousSibling;
if (prev && prev.nodeType == Node.TEXT_NODE && prev.textContent.split('\n').length <= 2) {
prev = arg.previousSibling.previousSibling;
if (prev && prev.nodeType == Node.COMMENT_NODE && prev != next /* don't use one comment twice */) {
prevArgPrevComment = prev;
comment = prev.textContent;
}
}
// get comment from next sibling comment without line breaks in-between (more priority)
var next = arg.nextSibling;
if (next.nodeType == Node.TEXT_NODE && next.textContent.indexOf('\n') == -1) {
next = next.nextSibling;
}
if (next.nodeType == Node.COMMENT_NODE) {
comment = next.textContent;
}
// get options
var options = comment.match(/^(.*?): ((.*), (.*))$/);
if (options) {
comment = options[1];
options = options[2].split(',');
options = options.map(function(option) { return option.trim(); });
}
if (comment.indexOf('noeditor') != -1) continue;
if (p.hide_uncommented && !comment) continue;
var path = `launch > arg[name=${name}]`;
html += `<label><code title="${name}">${name}</code>`;
if (type == 'bool') {
html += `<span class=switch><input data-item="${esc(item)}" data-path="${path}" type=checkbox ${value == 'true' ? 'checked' : ''}><i></i></span>`;
// html += `<label>${unslugName}<input value=${value} list=bool></label>`;
} else if (type == 'int') {
html += `<input data-item="${esc(item)}" data-path="${path}" type=number value="${esc(value)}">`;
} else if (type == 'float') {
let step = '0.1';
try { step = '0.' + '0'.repeat(value.match(/\.(.*)$/)[1].length - 1) + '1'; } catch {}; // TODO: scientific
html += `<input data-item="${esc(item)}" data-path="${path}" type=number step="${step}" value="${esc(value)}">`;
} else if (options) {
if (!options.includes(value)) {
options.unshift(value);
}
var optionsHTML = options.map(function(option) {
return `<option${option == value ? ' selected' : ''}>${esc(option)}</option>`;
}).join('');
html += `<select data-item="${esc(item)}" data-path="${path}">${optionsHTML}</select>`;
} else {
html += `<input data-item="${esc(item)}" data-path="${path}" value="${esc(value)}">`;
}
html += `<span class=comment title="${esc(comment)}">${esc(comment)}</span></label>`
}
return html;
}
function updateDocs() {
editorElem.querySelectorAll('input, select').forEach(function(elem) {
var type = elem.getAttribute('type');
if (type == 'checkbox') {
var value = String(elem.checked);
} else {
var value = elem.value;
}
var path = elem.getAttribute('data-path');
var item = elem.getAttribute('data-item');
var element = items[item].doc.querySelector(path);
if (element.getAttribute('default') === null && value == '') {
// don't add empty default if it's not set
return;
}
element.setAttribute('default', value);
});
}
editorElem.addEventListener('change', updateDocs);
function addLaunchFile(name, content) {
html = `<fieldset><legend><code>${name}</code></legend>`;
html += generateForm(name, content);
html += '</fieldset>'
editorElem.innerHTML += html;
}
function parseItem(str) {
var parsed = str.match(/(.*?)\/(.*)/);
var package = parsed[1];
var name = parsed[2];
return items[str] = { package, name };
}
var clover;
function determineMode() {
return new Promise(function(resolve, reject) {
function errcb(err) {
alert(err);
reject();
}
// check roslaunch_editor's read service
ros.getServiceType('/roslaunch_editor/read', function(t) {
if (t == 'roslaunch_editor/ReadLaunchFiles') {
clover = false;
resolve();
return;
}
// check clover's exec service
ros.getServiceType('/exec', function(t) {
if (t == 'clover/Execute') {
clover = true;
resolve();
return;
}
alert('Neither /roslaunch_editor/read nor /exec service not found');
reject();
}, errcb);
}, errcb);
});
}
function errCallback(err) {
alert('Error calling service: ' + err);
}
function loadItems() {
editorElem.innerHTML = '';
if (typeof p.items == 'string') {
p.items = p.items.split(',');
}
if (clover) {
const boundary = '===BOUNDARY===';
var cmd = 'bash -ic "' + p.items.map(function(item) {
parseItem(item);
return `roscat ${items[item].package} ${items[item].name}`;
}).join(` && echo -n ${boundary} && `) + '"';
exec.callService(new ROSLIB.ServiceRequest({ cmd }), function(res) {
if (res.code != 0) {
alert('Error reading launch-files');
return;
}
res.output.split(boundary).forEach(function(content, i) {
addLaunchFile(p.items[i], content);
});
document.body.classList.add('loaded');
}, errCallback);
} else {
var req = new ROSLIB.ServiceRequest();
req.files = p.items.map(function(item) {
parseItem(item);
return { 'package': items[item].package, 'name': items[item].name }
});
readLaunchFiles.callService(req, function(res) {
res.files.forEach(function(item, i) {
addLaunchFile(p.items[i], item.content);
});
document.body.classList.add('loaded');
}, errCallback);
}
}
// load params
Promise.all([
determineMode(),
readParam('apply_command', false, ''),
readParam('items', true),
readParam('hide_uncommented', true, false),
readParam('standalone', false, false),
]).then(() => loadItems());
function shellEscape(a) {
// https://github.com/xxorax/node-shell-escape
var ret = [];
a.forEach(function (s) {
if (!['&&', '||', '|', '>', '<', '>>', '<<'].includes(s) && /[^A-Za-z0-9_\/:=-]/.test(s)) {
s = "'" + s.replace(/'/g, "'\\''") + "'";
s = s.replace(/^(?:'')+/g, '') // unduplicate single-quote at the beginning
.replace(/\\'''/g, "\\'"); // remove non-escaped single-quote if there are enclosed between 2 escaped
}
ret.push(s);
});
return ret.join(' ');
}
var applying = false;
// TODO: reread launch file on connected
function apply() {
applying = true;
document.body.classList.remove('loaded');
updateDocs();
if (clover) {
var script = '';
for (item in items) {
if (script) script += ' && ';
var content = items[item].doc.documentElement.outerHTML;
script += `_roscmd ${items[item].package} ${items[item].name} && echo ${shellEscape([content])} > $arg`;
}
if (p.apply_command) {
script + ' && ' + p.apply_command;
}
var cmd = shellEscape(['bash', '-ic', script]);
exec.callService(new ROSLIB.ServiceRequest({ cmd: cmd }), function(res) {
if (res.code != 0) {
alert('Error');
return;
}
document.body.classList.add('loaded');
});
} else {
var req = new ROSLIB.ServiceRequest();
req.files = Object.keys(items).map(function(key) {
var item = items[key];
return { package: item.package, name: item.name, content: item.doc.documentElement.outerHTML }
});
writeLaunchFiles.callService(req, function(res) {
if (!res.success) {
alert('Error writing files ' + res.message);
}
document.body.classList.add('loaded');
applying = false;
}, errCallback);
}
}
ros.on('connection', function() {
if (applying) {
// connection after applying tells that system restarted
document.body.classList.add('loaded');
applying = false;
}
})

View File

@@ -0,0 +1,46 @@
var url = 'ws://' + location.hostname + ':9090';
var ros = new ROSLIB.Ros({ url });
ros.on('connection', function () {
document.body.classList.add('connected');
});
ros.on('close', function () {
document.body.classList.remove('connected');
setTimeout(function() {
try {
ros.connect(url);
} catch (e) {}
}, 2000);
});
var exec = new ROSLIB.Service({ ros: ros, name : '/exec', serviceType : 'clover/Execute' });
var readLaunchFiles = new ROSLIB.Service({ ros: ros, name: '/roslaunch_editor/read', serviceType: 'roslaunch_editor/ReadLaunchFiles' });
var writeLaunchFiles = new ROSLIB.Service({ ros: ros, name: '/roslaunch_editor/write', serviceType: 'roslaunch_editor/WriteLaunchFiles' });
var p = {}; // parameters storage
function readParam(name, fromUrl, _default) {
return new Promise(function(resolve, reject) {
// read from url
if (fromUrl && ((p[name] = new URL(window.location.href).searchParams.get(name)) !== null)) {
resolve();
return;
}
// read from ROS params
new ROSLIB.Param({ ros: ros, name: '/roslaunch_editor/' + name }).get(function(val) {
if (val === null) {
if (_default === undefined) {
alert('Cannot read required parameter ' + name);
reject();
} else {
p[name] = _default;
resolve();
}
return;
}
p[name] = val;
resolve();
})
});
}

4560
roslaunch_editor/www/roslib.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
.switch {
display: inline-block;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.switch i {
position: relative;
display: inline-block;
margin-right: .5rem;
width: 46px;
height: 26px;
background-color: #e6e6e6;
border-radius: 23px;
vertical-align: text-bottom;
transition: all 0.3s linear;
}
.switch i::before {
content: "";
position: absolute;
left: 0;
width: 42px;
height: 22px;
background-color: #fff;
border-radius: 11px;
transform: translate3d(2px, 2px, 0) scale3d(1, 1, 1);
transition: all 0.25s linear;
}
.switch i::after {
content: "";
position: absolute;
left: 0;
width: 22px;
height: 22px;
background-color: #fff;
border-radius: 11px;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.24);
transform: translate3d(2px, 2px, 0);
transition: all 0.2s ease-in-out;
}
.switch:active i::after {
width: 28px;
transform: translate3d(2px, 2px, 0);
}
.switch:active input:checked + i::after { transform: translate3d(16px, 2px, 0); }
.switch input { display: none; }
.switch input:checked + i { background-color: #4B9DD7; }
.switch input:checked + i::before { transform: translate3d(18px, 2px, 0) scale3d(0, 0, 0); }
.switch input:checked + i::after { transform: translate3d(22px, 2px, 0); }