From fa9a1794f6c627e7b31815427c00d66120780f47 Mon Sep 17 00:00:00 2001 From: Oleg Kalachev Date: Tue, 21 Jul 2020 10:00:33 +0300 Subject: [PATCH 1/4] Add roslaunch_editor --- roslaunch_editor/CMakeLists.txt | 82 + roslaunch_editor/README.md | 28 + roslaunch_editor/launch/example.launch | 11 + roslaunch_editor/msg/LaunchFile.msg | 3 + roslaunch_editor/package.xml | 45 + roslaunch_editor/roslaunch_editor.jpg | Bin 0 -> 165330 bytes roslaunch_editor/src/editor | 73 + roslaunch_editor/srv/ReadLaunchFiles.srv | 7 + roslaunch_editor/srv/WriteLaunchFiles.srv | 6 + roslaunch_editor/test/test.launch | 34 + roslaunch_editor/www/index.html | 140 + roslaunch_editor/www/loader.css | 67 + roslaunch_editor/www/main.js | 287 ++ roslaunch_editor/www/ros.js | 46 + roslaunch_editor/www/roslib.js | 4560 +++++++++++++++++++++ roslaunch_editor/www/switch.css | 57 + 16 files changed, 5446 insertions(+) create mode 100644 roslaunch_editor/CMakeLists.txt create mode 100644 roslaunch_editor/README.md create mode 100644 roslaunch_editor/launch/example.launch create mode 100644 roslaunch_editor/msg/LaunchFile.msg create mode 100644 roslaunch_editor/package.xml create mode 100644 roslaunch_editor/roslaunch_editor.jpg create mode 100755 roslaunch_editor/src/editor create mode 100644 roslaunch_editor/srv/ReadLaunchFiles.srv create mode 100644 roslaunch_editor/srv/WriteLaunchFiles.srv create mode 100644 roslaunch_editor/test/test.launch create mode 100644 roslaunch_editor/www/index.html create mode 100644 roslaunch_editor/www/loader.css create mode 100644 roslaunch_editor/www/main.js create mode 100644 roslaunch_editor/www/ros.js create mode 100644 roslaunch_editor/www/roslib.js create mode 100644 roslaunch_editor/www/switch.css diff --git a/roslaunch_editor/CMakeLists.txt b/roslaunch_editor/CMakeLists.txt new file mode 100644 index 000000000..ea70c5600 --- /dev/null +++ b/roslaunch_editor/CMakeLists.txt @@ -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) diff --git a/roslaunch_editor/README.md b/roslaunch_editor/README.md new file mode 100644 index 000000000..9a9e49ca7 --- /dev/null +++ b/roslaunch_editor/README.md @@ -0,0 +1,28 @@ +# roslaunch_editor + +Web-based ROS launch-files editor, created for making configuration of your robot more user-friendly for novices. + + + +## 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`. diff --git a/roslaunch_editor/launch/example.launch b/roslaunch_editor/launch/example.launch new file mode 100644 index 000000000..ff485bfd2 --- /dev/null +++ b/roslaunch_editor/launch/example.launch @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/roslaunch_editor/msg/LaunchFile.msg b/roslaunch_editor/msg/LaunchFile.msg new file mode 100644 index 000000000..91c8ccf74 --- /dev/null +++ b/roslaunch_editor/msg/LaunchFile.msg @@ -0,0 +1,3 @@ +string package +string name +string content diff --git a/roslaunch_editor/package.xml b/roslaunch_editor/package.xml new file mode 100644 index 000000000..8baa2a8dc --- /dev/null +++ b/roslaunch_editor/package.xml @@ -0,0 +1,45 @@ + + + roslaunch_editor + 0.0.0 + Web based roslaunch files editor + Oleg Kalachev + MIT + + + + https://github.com/CopterExpress/clover + + + + Oleg Kalachev + + + + + + + + + + + + + + + + + + + + + + catkin + message_generation + + + + + + + diff --git a/roslaunch_editor/roslaunch_editor.jpg b/roslaunch_editor/roslaunch_editor.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dfae84cae7af1af2dadeb20999180a4e909123ea GIT binary patch literal 165330 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∋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 E4K$}+ng9R* literal 0 HcmV?d00001 diff --git a/roslaunch_editor/src/editor b/roslaunch_editor/src/editor new file mode 100755 index 000000000..15d90d76c --- /dev/null +++ b/roslaunch_editor/src/editor @@ -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() diff --git a/roslaunch_editor/srv/ReadLaunchFiles.srv b/roslaunch_editor/srv/ReadLaunchFiles.srv new file mode 100644 index 000000000..d9f9c7631 --- /dev/null +++ b/roslaunch_editor/srv/ReadLaunchFiles.srv @@ -0,0 +1,7 @@ +# Read launch-files + +LaunchFile[] files # content field is ignored +--- +bool success +string message +LaunchFile[] files diff --git a/roslaunch_editor/srv/WriteLaunchFiles.srv b/roslaunch_editor/srv/WriteLaunchFiles.srv new file mode 100644 index 000000000..36d18cee3 --- /dev/null +++ b/roslaunch_editor/srv/WriteLaunchFiles.srv @@ -0,0 +1,6 @@ +# Write launch-files and apply (restart the system) + +LaunchFile[] files +--- +bool success +string message diff --git a/roslaunch_editor/test/test.launch b/roslaunch_editor/test/test.launch new file mode 100644 index 000000000..3866657e9 --- /dev/null +++ b/roslaunch_editor/test/test.launch @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/roslaunch_editor/www/index.html b/roslaunch_editor/www/index.html new file mode 100644 index 000000000..ebb6b478b --- /dev/null +++ b/roslaunch_editor/www/index.html @@ -0,0 +1,140 @@ + + + + + + + roslaunch editor + + +
+
+
+ +
+
+
+
+ + + + diff --git a/roslaunch_editor/www/loader.css b/roslaunch_editor/www/loader.css new file mode 100644 index 000000000..0ef988330 --- /dev/null +++ b/roslaunch_editor/www/loader.css @@ -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; + } +} diff --git a/roslaunch_editor/www/main.js b/roslaunch_editor/www/main.js new file mode 100644 index 000000000..74f0ae1b5 --- /dev/null +++ b/roslaunch_editor/www/main.js @@ -0,0 +1,287 @@ +var editorElem = document.querySelector('form.editor'); + +// escape html +function esc(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + +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 += `` + } + + 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 = `
${name}`; + html += generateForm(name, content); + html += '
' + 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; + } +}) diff --git a/roslaunch_editor/www/ros.js b/roslaunch_editor/www/ros.js new file mode 100644 index 000000000..520f10546 --- /dev/null +++ b/roslaunch_editor/www/ros.js @@ -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(); + }) + }); +} diff --git a/roslaunch_editor/www/roslib.js b/roslaunch_editor/www/roslib.js new file mode 100644 index 000000000..175d3d742 --- /dev/null +++ b/roslaunch_editor/www/roslib.js @@ -0,0 +1,4560 @@ +(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +(function(global, undefined) { "use strict"; +var POW_2_24 = Math.pow(2, -24), + POW_2_32 = Math.pow(2, 32), + POW_2_53 = Math.pow(2, 53); + +function encode(value) { + var data = new ArrayBuffer(256); + var dataView = new DataView(data); + var lastLength; + var offset = 0; + + function ensureSpace(length) { + var newByteLength = data.byteLength; + var requiredLength = offset + length; + while (newByteLength < requiredLength) + newByteLength *= 2; + if (newByteLength !== data.byteLength) { + var oldDataView = dataView; + data = new ArrayBuffer(newByteLength); + dataView = new DataView(data); + var uint32count = (offset + 3) >> 2; + for (var i = 0; i < uint32count; ++i) + dataView.setUint32(i * 4, oldDataView.getUint32(i * 4)); + } + + lastLength = length; + return dataView; + } + function write() { + offset += lastLength; + } + function writeFloat64(value) { + write(ensureSpace(8).setFloat64(offset, value)); + } + function writeUint8(value) { + write(ensureSpace(1).setUint8(offset, value)); + } + function writeUint8Array(value) { + var dataView = ensureSpace(value.length); + for (var i = 0; i < value.length; ++i) + dataView.setUint8(offset + i, value[i]); + write(); + } + function writeUint16(value) { + write(ensureSpace(2).setUint16(offset, value)); + } + function writeUint32(value) { + write(ensureSpace(4).setUint32(offset, value)); + } + function writeUint64(value) { + var low = value % POW_2_32; + var high = (value - low) / POW_2_32; + var dataView = ensureSpace(8); + dataView.setUint32(offset, high); + dataView.setUint32(offset + 4, low); + write(); + } + function writeTypeAndLength(type, length) { + if (length < 24) { + writeUint8(type << 5 | length); + } else if (length < 0x100) { + writeUint8(type << 5 | 24); + writeUint8(length); + } else if (length < 0x10000) { + writeUint8(type << 5 | 25); + writeUint16(length); + } else if (length < 0x100000000) { + writeUint8(type << 5 | 26); + writeUint32(length); + } else { + writeUint8(type << 5 | 27); + writeUint64(length); + } + } + + function encodeItem(value) { + var i; + + if (value === false) + return writeUint8(0xf4); + if (value === true) + return writeUint8(0xf5); + if (value === null) + return writeUint8(0xf6); + if (value === undefined) + return writeUint8(0xf7); + + switch (typeof value) { + case "number": + if (Math.floor(value) === value) { + if (0 <= value && value <= POW_2_53) + return writeTypeAndLength(0, value); + if (-POW_2_53 <= value && value < 0) + return writeTypeAndLength(1, -(value + 1)); + } + writeUint8(0xfb); + return writeFloat64(value); + + case "string": + var utf8data = []; + for (i = 0; i < value.length; ++i) { + var charCode = value.charCodeAt(i); + if (charCode < 0x80) { + utf8data.push(charCode); + } else if (charCode < 0x800) { + utf8data.push(0xc0 | charCode >> 6); + utf8data.push(0x80 | charCode & 0x3f); + } else if (charCode < 0xd800) { + utf8data.push(0xe0 | charCode >> 12); + utf8data.push(0x80 | (charCode >> 6) & 0x3f); + utf8data.push(0x80 | charCode & 0x3f); + } else { + charCode = (charCode & 0x3ff) << 10; + charCode |= value.charCodeAt(++i) & 0x3ff; + charCode += 0x10000; + + utf8data.push(0xf0 | charCode >> 18); + utf8data.push(0x80 | (charCode >> 12) & 0x3f); + utf8data.push(0x80 | (charCode >> 6) & 0x3f); + utf8data.push(0x80 | charCode & 0x3f); + } + } + + writeTypeAndLength(3, utf8data.length); + return writeUint8Array(utf8data); + + default: + var length; + if (Array.isArray(value)) { + length = value.length; + writeTypeAndLength(4, length); + for (i = 0; i < length; ++i) + encodeItem(value[i]); + } else if (value instanceof Uint8Array) { + writeTypeAndLength(2, value.length); + writeUint8Array(value); + } else { + var keys = Object.keys(value); + length = keys.length; + writeTypeAndLength(5, length); + for (i = 0; i < length; ++i) { + var key = keys[i]; + encodeItem(key); + encodeItem(value[key]); + } + } + } + } + + encodeItem(value); + + if ("slice" in data) + return data.slice(0, offset); + + var ret = new ArrayBuffer(offset); + var retView = new DataView(ret); + for (var i = 0; i < offset; ++i) + retView.setUint8(i, dataView.getUint8(i)); + return ret; +} + +function decode(data, tagger, simpleValue) { + var dataView = new DataView(data); + var offset = 0; + + if (typeof tagger !== "function") + tagger = function(value) { return value; }; + if (typeof simpleValue !== "function") + simpleValue = function() { return undefined; }; + + function read(value, length) { + offset += length; + return value; + } + function readArrayBuffer(length) { + return read(new Uint8Array(data, offset, length), length); + } + function readFloat16() { + var tempArrayBuffer = new ArrayBuffer(4); + var tempDataView = new DataView(tempArrayBuffer); + var value = readUint16(); + + var sign = value & 0x8000; + var exponent = value & 0x7c00; + var fraction = value & 0x03ff; + + if (exponent === 0x7c00) + exponent = 0xff << 10; + else if (exponent !== 0) + exponent += (127 - 15) << 10; + else if (fraction !== 0) + return fraction * POW_2_24; + + tempDataView.setUint32(0, sign << 16 | exponent << 13 | fraction << 13); + return tempDataView.getFloat32(0); + } + function readFloat32() { + return read(dataView.getFloat32(offset), 4); + } + function readFloat64() { + return read(dataView.getFloat64(offset), 8); + } + function readUint8() { + return read(dataView.getUint8(offset), 1); + } + function readUint16() { + return read(dataView.getUint16(offset), 2); + } + function readUint32() { + return read(dataView.getUint32(offset), 4); + } + function readUint64() { + return readUint32() * POW_2_32 + readUint32(); + } + function readBreak() { + if (dataView.getUint8(offset) !== 0xff) + return false; + offset += 1; + return true; + } + function readLength(additionalInformation) { + if (additionalInformation < 24) + return additionalInformation; + if (additionalInformation === 24) + return readUint8(); + if (additionalInformation === 25) + return readUint16(); + if (additionalInformation === 26) + return readUint32(); + if (additionalInformation === 27) + return readUint64(); + if (additionalInformation === 31) + return -1; + throw "Invalid length encoding"; + } + function readIndefiniteStringLength(majorType) { + var initialByte = readUint8(); + if (initialByte === 0xff) + return -1; + var length = readLength(initialByte & 0x1f); + if (length < 0 || (initialByte >> 5) !== majorType) + throw "Invalid indefinite length element"; + return length; + } + + function appendUtf16data(utf16data, length) { + for (var i = 0; i < length; ++i) { + var value = readUint8(); + if (value & 0x80) { + if (value < 0xe0) { + value = (value & 0x1f) << 6 + | (readUint8() & 0x3f); + length -= 1; + } else if (value < 0xf0) { + value = (value & 0x0f) << 12 + | (readUint8() & 0x3f) << 6 + | (readUint8() & 0x3f); + length -= 2; + } else { + value = (value & 0x0f) << 18 + | (readUint8() & 0x3f) << 12 + | (readUint8() & 0x3f) << 6 + | (readUint8() & 0x3f); + length -= 3; + } + } + + if (value < 0x10000) { + utf16data.push(value); + } else { + value -= 0x10000; + utf16data.push(0xd800 | (value >> 10)); + utf16data.push(0xdc00 | (value & 0x3ff)); + } + } + } + + function decodeItem() { + var initialByte = readUint8(); + var majorType = initialByte >> 5; + var additionalInformation = initialByte & 0x1f; + var i; + var length; + + if (majorType === 7) { + switch (additionalInformation) { + case 25: + return readFloat16(); + case 26: + return readFloat32(); + case 27: + return readFloat64(); + } + } + + length = readLength(additionalInformation); + if (length < 0 && (majorType < 2 || 6 < majorType)) + throw "Invalid length"; + + switch (majorType) { + case 0: + return length; + case 1: + return -1 - length; + case 2: + if (length < 0) { + var elements = []; + var fullArrayLength = 0; + while ((length = readIndefiniteStringLength(majorType)) >= 0) { + fullArrayLength += length; + elements.push(readArrayBuffer(length)); + } + var fullArray = new Uint8Array(fullArrayLength); + var fullArrayOffset = 0; + for (i = 0; i < elements.length; ++i) { + fullArray.set(elements[i], fullArrayOffset); + fullArrayOffset += elements[i].length; + } + return fullArray; + } + return readArrayBuffer(length); + case 3: + var utf16data = []; + if (length < 0) { + while ((length = readIndefiniteStringLength(majorType)) >= 0) + appendUtf16data(utf16data, length); + } else + appendUtf16data(utf16data, length); + return String.fromCharCode.apply(null, utf16data); + case 4: + var retArray; + if (length < 0) { + retArray = []; + while (!readBreak()) + retArray.push(decodeItem()); + } else { + retArray = new Array(length); + for (i = 0; i < length; ++i) + retArray[i] = decodeItem(); + } + return retArray; + case 5: + var retObject = {}; + for (i = 0; i < length || length < 0 && !readBreak(); ++i) { + var key = decodeItem(); + retObject[key] = decodeItem(); + } + return retObject; + case 6: + return tagger(decodeItem(), length); + case 7: + switch (length) { + case 20: + return false; + case 21: + return true; + case 22: + return null; + case 23: + return undefined; + default: + return simpleValue(length); + } + } + } + + var ret = decodeItem(); + if (offset !== data.byteLength) + throw "Remaining bytes"; + return ret; +} + +var obj = { encode: encode, decode: decode }; + +if (typeof define === "function" && define.amd) + define("cbor/cbor", obj); +else if (typeof module !== 'undefined' && module.exports) + module.exports = obj; +else if (!global.CBOR) + global.CBOR = obj; + +})(this); + +},{}],2:[function(require,module,exports){ +(function (process){ +/*! + * EventEmitter2 + * https://github.com/hij1nx/EventEmitter2 + * + * Copyright (c) 2013 hij1nx + * Licensed under the MIT license. + */ +;!function(undefined) { + + var isArray = Array.isArray ? Array.isArray : function _isArray(obj) { + return Object.prototype.toString.call(obj) === "[object Array]"; + }; + var defaultMaxListeners = 10; + + function init() { + this._events = {}; + if (this._conf) { + configure.call(this, this._conf); + } + } + + function configure(conf) { + if (conf) { + this._conf = conf; + + conf.delimiter && (this.delimiter = conf.delimiter); + this._maxListeners = conf.maxListeners !== undefined ? conf.maxListeners : defaultMaxListeners; + + conf.wildcard && (this.wildcard = conf.wildcard); + conf.newListener && (this.newListener = conf.newListener); + conf.verboseMemoryLeak && (this.verboseMemoryLeak = conf.verboseMemoryLeak); + + if (this.wildcard) { + this.listenerTree = {}; + } + } else { + this._maxListeners = defaultMaxListeners; + } + } + + function logPossibleMemoryLeak(count, eventName) { + var errorMsg = '(node) warning: possible EventEmitter memory ' + + 'leak detected. ' + count + ' listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.'; + + if(this.verboseMemoryLeak){ + errorMsg += ' Event name: ' + eventName + '.'; + } + + if(typeof process !== 'undefined' && process.emitWarning){ + var e = new Error(errorMsg); + e.name = 'MaxListenersExceededWarning'; + e.emitter = this; + e.count = count; + process.emitWarning(e); + } else { + console.error(errorMsg); + + if (console.trace){ + console.trace(); + } + } + } + + function EventEmitter(conf) { + this._events = {}; + this.newListener = false; + this.verboseMemoryLeak = false; + configure.call(this, conf); + } + EventEmitter.EventEmitter2 = EventEmitter; // backwards compatibility for exporting EventEmitter property + + // + // Attention, function return type now is array, always ! + // It has zero elements if no any matches found and one or more + // elements (leafs) if there are matches + // + function searchListenerTree(handlers, type, tree, i) { + if (!tree) { + return []; + } + var listeners=[], leaf, len, branch, xTree, xxTree, isolatedBranch, endReached, + typeLength = type.length, currentType = type[i], nextType = type[i+1]; + if (i === typeLength && tree._listeners) { + // + // If at the end of the event(s) list and the tree has listeners + // invoke those listeners. + // + if (typeof tree._listeners === 'function') { + handlers && handlers.push(tree._listeners); + return [tree]; + } else { + for (leaf = 0, len = tree._listeners.length; leaf < len; leaf++) { + handlers && handlers.push(tree._listeners[leaf]); + } + return [tree]; + } + } + + if ((currentType === '*' || currentType === '**') || tree[currentType]) { + // + // If the event emitted is '*' at this part + // or there is a concrete match at this patch + // + if (currentType === '*') { + for (branch in tree) { + if (branch !== '_listeners' && tree.hasOwnProperty(branch)) { + listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i+1)); + } + } + return listeners; + } else if(currentType === '**') { + endReached = (i+1 === typeLength || (i+2 === typeLength && nextType === '*')); + if(endReached && tree._listeners) { + // The next element has a _listeners, add it to the handlers. + listeners = listeners.concat(searchListenerTree(handlers, type, tree, typeLength)); + } + + for (branch in tree) { + if (branch !== '_listeners' && tree.hasOwnProperty(branch)) { + if(branch === '*' || branch === '**') { + if(tree[branch]._listeners && !endReached) { + listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], typeLength)); + } + listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i)); + } else if(branch === nextType) { + listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i+2)); + } else { + // No match on this one, shift into the tree but not in the type array. + listeners = listeners.concat(searchListenerTree(handlers, type, tree[branch], i)); + } + } + } + return listeners; + } + + listeners = listeners.concat(searchListenerTree(handlers, type, tree[currentType], i+1)); + } + + xTree = tree['*']; + if (xTree) { + // + // If the listener tree will allow any match for this part, + // then recursively explore all branches of the tree + // + searchListenerTree(handlers, type, xTree, i+1); + } + + xxTree = tree['**']; + if(xxTree) { + if(i < typeLength) { + if(xxTree._listeners) { + // If we have a listener on a '**', it will catch all, so add its handler. + searchListenerTree(handlers, type, xxTree, typeLength); + } + + // Build arrays of matching next branches and others. + for(branch in xxTree) { + if(branch !== '_listeners' && xxTree.hasOwnProperty(branch)) { + if(branch === nextType) { + // We know the next element will match, so jump twice. + searchListenerTree(handlers, type, xxTree[branch], i+2); + } else if(branch === currentType) { + // Current node matches, move into the tree. + searchListenerTree(handlers, type, xxTree[branch], i+1); + } else { + isolatedBranch = {}; + isolatedBranch[branch] = xxTree[branch]; + searchListenerTree(handlers, type, { '**': isolatedBranch }, i+1); + } + } + } + } else if(xxTree._listeners) { + // We have reached the end and still on a '**' + searchListenerTree(handlers, type, xxTree, typeLength); + } else if(xxTree['*'] && xxTree['*']._listeners) { + searchListenerTree(handlers, type, xxTree['*'], typeLength); + } + } + + return listeners; + } + + function growListenerTree(type, listener) { + + type = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + + // + // Looks for two consecutive '**', if so, don't add the event at all. + // + for(var i = 0, len = type.length; i+1 < len; i++) { + if(type[i] === '**' && type[i+1] === '**') { + return; + } + } + + var tree = this.listenerTree; + var name = type.shift(); + + while (name !== undefined) { + + if (!tree[name]) { + tree[name] = {}; + } + + tree = tree[name]; + + if (type.length === 0) { + + if (!tree._listeners) { + tree._listeners = listener; + } + else { + if (typeof tree._listeners === 'function') { + tree._listeners = [tree._listeners]; + } + + tree._listeners.push(listener); + + if ( + !tree._listeners.warned && + this._maxListeners > 0 && + tree._listeners.length > this._maxListeners + ) { + tree._listeners.warned = true; + logPossibleMemoryLeak.call(this, tree._listeners.length, name); + } + } + return true; + } + name = type.shift(); + } + return true; + } + + // By default EventEmitters will print a warning if more than + // 10 listeners are added to it. This is a useful default which + // helps finding memory leaks. + // + // Obviously not all Emitters should be limited to 10. This function allows + // that to be increased. Set to zero for unlimited. + + EventEmitter.prototype.delimiter = '.'; + + EventEmitter.prototype.setMaxListeners = function(n) { + if (n !== undefined) { + this._maxListeners = n; + if (!this._conf) this._conf = {}; + this._conf.maxListeners = n; + } + }; + + EventEmitter.prototype.event = ''; + + + EventEmitter.prototype.once = function(event, fn) { + return this._once(event, fn, false); + }; + + EventEmitter.prototype.prependOnceListener = function(event, fn) { + return this._once(event, fn, true); + }; + + EventEmitter.prototype._once = function(event, fn, prepend) { + this._many(event, 1, fn, prepend); + return this; + }; + + EventEmitter.prototype.many = function(event, ttl, fn) { + return this._many(event, ttl, fn, false); + } + + EventEmitter.prototype.prependMany = function(event, ttl, fn) { + return this._many(event, ttl, fn, true); + } + + EventEmitter.prototype._many = function(event, ttl, fn, prepend) { + var self = this; + + if (typeof fn !== 'function') { + throw new Error('many only accepts instances of Function'); + } + + function listener() { + if (--ttl === 0) { + self.off(event, listener); + } + return fn.apply(this, arguments); + } + + listener._origin = fn; + + this._on(event, listener, prepend); + + return self; + }; + + EventEmitter.prototype.emit = function() { + + this._events || init.call(this); + + var type = arguments[0]; + + if (type === 'newListener' && !this.newListener) { + if (!this._events.newListener) { + return false; + } + } + + var al = arguments.length; + var args,l,i,j; + var handler; + + if (this._all && this._all.length) { + handler = this._all.slice(); + if (al > 3) { + args = new Array(al); + for (j = 0; j < al; j++) args[j] = arguments[j]; + } + + for (i = 0, l = handler.length; i < l; i++) { + this.event = type; + switch (al) { + case 1: + handler[i].call(this, type); + break; + case 2: + handler[i].call(this, type, arguments[1]); + break; + case 3: + handler[i].call(this, type, arguments[1], arguments[2]); + break; + default: + handler[i].apply(this, args); + } + } + } + + if (this.wildcard) { + handler = []; + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + searchListenerTree.call(this, handler, ns, this.listenerTree, 0); + } else { + handler = this._events[type]; + if (typeof handler === 'function') { + this.event = type; + switch (al) { + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + default: + args = new Array(al - 1); + for (j = 1; j < al; j++) args[j - 1] = arguments[j]; + handler.apply(this, args); + } + return true; + } else if (handler) { + // need to make copy of handlers because list can change in the middle + // of emit call + handler = handler.slice(); + } + } + + if (handler && handler.length) { + if (al > 3) { + args = new Array(al - 1); + for (j = 1; j < al; j++) args[j - 1] = arguments[j]; + } + for (i = 0, l = handler.length; i < l; i++) { + this.event = type; + switch (al) { + case 1: + handler[i].call(this); + break; + case 2: + handler[i].call(this, arguments[1]); + break; + case 3: + handler[i].call(this, arguments[1], arguments[2]); + break; + default: + handler[i].apply(this, args); + } + } + return true; + } else if (!this._all && type === 'error') { + if (arguments[1] instanceof Error) { + throw arguments[1]; // Unhandled 'error' event + } else { + throw new Error("Uncaught, unspecified 'error' event."); + } + return false; + } + + return !!this._all; + }; + + EventEmitter.prototype.emitAsync = function() { + + this._events || init.call(this); + + var type = arguments[0]; + + if (type === 'newListener' && !this.newListener) { + if (!this._events.newListener) { return Promise.resolve([false]); } + } + + var promises= []; + + var al = arguments.length; + var args,l,i,j; + var handler; + + if (this._all) { + if (al > 3) { + args = new Array(al); + for (j = 1; j < al; j++) args[j] = arguments[j]; + } + for (i = 0, l = this._all.length; i < l; i++) { + this.event = type; + switch (al) { + case 1: + promises.push(this._all[i].call(this, type)); + break; + case 2: + promises.push(this._all[i].call(this, type, arguments[1])); + break; + case 3: + promises.push(this._all[i].call(this, type, arguments[1], arguments[2])); + break; + default: + promises.push(this._all[i].apply(this, args)); + } + } + } + + if (this.wildcard) { + handler = []; + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + searchListenerTree.call(this, handler, ns, this.listenerTree, 0); + } else { + handler = this._events[type]; + } + + if (typeof handler === 'function') { + this.event = type; + switch (al) { + case 1: + promises.push(handler.call(this)); + break; + case 2: + promises.push(handler.call(this, arguments[1])); + break; + case 3: + promises.push(handler.call(this, arguments[1], arguments[2])); + break; + default: + args = new Array(al - 1); + for (j = 1; j < al; j++) args[j - 1] = arguments[j]; + promises.push(handler.apply(this, args)); + } + } else if (handler && handler.length) { + handler = handler.slice(); + if (al > 3) { + args = new Array(al - 1); + for (j = 1; j < al; j++) args[j - 1] = arguments[j]; + } + for (i = 0, l = handler.length; i < l; i++) { + this.event = type; + switch (al) { + case 1: + promises.push(handler[i].call(this)); + break; + case 2: + promises.push(handler[i].call(this, arguments[1])); + break; + case 3: + promises.push(handler[i].call(this, arguments[1], arguments[2])); + break; + default: + promises.push(handler[i].apply(this, args)); + } + } + } else if (!this._all && type === 'error') { + if (arguments[1] instanceof Error) { + return Promise.reject(arguments[1]); // Unhandled 'error' event + } else { + return Promise.reject("Uncaught, unspecified 'error' event."); + } + } + + return Promise.all(promises); + }; + + EventEmitter.prototype.on = function(type, listener) { + return this._on(type, listener, false); + }; + + EventEmitter.prototype.prependListener = function(type, listener) { + return this._on(type, listener, true); + }; + + EventEmitter.prototype.onAny = function(fn) { + return this._onAny(fn, false); + }; + + EventEmitter.prototype.prependAny = function(fn) { + return this._onAny(fn, true); + }; + + EventEmitter.prototype.addListener = EventEmitter.prototype.on; + + EventEmitter.prototype._onAny = function(fn, prepend){ + if (typeof fn !== 'function') { + throw new Error('onAny only accepts instances of Function'); + } + + if (!this._all) { + this._all = []; + } + + // Add the function to the event listener collection. + if(prepend){ + this._all.unshift(fn); + }else{ + this._all.push(fn); + } + + return this; + } + + EventEmitter.prototype._on = function(type, listener, prepend) { + if (typeof type === 'function') { + this._onAny(type, listener); + return this; + } + + if (typeof listener !== 'function') { + throw new Error('on only accepts instances of Function'); + } + this._events || init.call(this); + + // To avoid recursion in the case that type == "newListeners"! Before + // adding it to the listeners, first emit "newListeners". + this.emit('newListener', type, listener); + + if (this.wildcard) { + growListenerTree.call(this, type, listener); + return this; + } + + if (!this._events[type]) { + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + } + else { + if (typeof this._events[type] === 'function') { + // Change to array. + this._events[type] = [this._events[type]]; + } + + // If we've already got an array, just add + if(prepend){ + this._events[type].unshift(listener); + }else{ + this._events[type].push(listener); + } + + // Check for listener leak + if ( + !this._events[type].warned && + this._maxListeners > 0 && + this._events[type].length > this._maxListeners + ) { + this._events[type].warned = true; + logPossibleMemoryLeak.call(this, this._events[type].length, type); + } + } + + return this; + } + + EventEmitter.prototype.off = function(type, listener) { + if (typeof listener !== 'function') { + throw new Error('removeListener only takes instances of Function'); + } + + var handlers,leafs=[]; + + if(this.wildcard) { + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + leafs = searchListenerTree.call(this, null, ns, this.listenerTree, 0); + } + else { + // does not use listeners(), so no side effect of creating _events[type] + if (!this._events[type]) return this; + handlers = this._events[type]; + leafs.push({_listeners:handlers}); + } + + for (var iLeaf=0; iLeaf 0) { + recursivelyGarbageCollect(root[key]); + } + if (Object.keys(obj).length === 0) { + delete root[key]; + } + } + } + recursivelyGarbageCollect(this.listenerTree); + + return this; + }; + + EventEmitter.prototype.offAny = function(fn) { + var i = 0, l = 0, fns; + if (fn && this._all && this._all.length > 0) { + fns = this._all; + for(i = 0, l = fns.length; i < l; i++) { + if(fn === fns[i]) { + fns.splice(i, 1); + this.emit("removeListenerAny", fn); + return this; + } + } + } else { + fns = this._all; + for(i = 0, l = fns.length; i < l; i++) + this.emit("removeListenerAny", fns[i]); + this._all = []; + } + return this; + }; + + EventEmitter.prototype.removeListener = EventEmitter.prototype.off; + + EventEmitter.prototype.removeAllListeners = function(type) { + if (arguments.length === 0) { + !this._events || init.call(this); + return this; + } + + if (this.wildcard) { + var ns = typeof type === 'string' ? type.split(this.delimiter) : type.slice(); + var leafs = searchListenerTree.call(this, null, ns, this.listenerTree, 0); + + for (var iLeaf=0; iLeaf 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + runTimeout(drainQueue); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; +process.prependListener = noop; +process.prependOnceListener = noop; + +process.listeners = function (name) { return [] } + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}],5:[function(require,module,exports){ +/** + * @fileOverview + * @author Russell Toris - rctoris@wpi.edu + */ + +/** + * If you use roslib in a browser, all the classes will be exported to a global variable called ROSLIB. + * + * If you use nodejs, this is the variable you get when you require('roslib') + */ +var ROSLIB = this.ROSLIB || { + REVISION : '1.0.1' +}; + +var assign = require('object-assign'); + +// Add core components +assign(ROSLIB, require('./core')); + +assign(ROSLIB, require('./actionlib')); + +assign(ROSLIB, require('./math')); + +assign(ROSLIB, require('./tf')); + +assign(ROSLIB, require('./urdf')); + +module.exports = ROSLIB; + +},{"./actionlib":11,"./core":20,"./math":25,"./tf":28,"./urdf":40,"object-assign":3}],6:[function(require,module,exports){ +(function (global){ +global.ROSLIB = require('./RosLib'); +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"./RosLib":5}],7:[function(require,module,exports){ +/** + * @fileOverview + * @author Russell Toris - rctoris@wpi.edu + */ + +var Topic = require('../core/Topic'); +var Message = require('../core/Message'); +var EventEmitter2 = require('eventemitter2').EventEmitter2; + +/** + * An actionlib action client. + * + * Emits the following events: + * * 'timeout' - if a timeout occurred while sending a goal + * * 'status' - the status messages received from the action server + * * 'feedback' - the feedback messages received from the action server + * * 'result' - the result returned from the action server + * + * @constructor + * @param options - object with following keys: + * * ros - the ROSLIB.Ros connection handle + * * serverName - the action server name, like /fibonacci + * * actionName - the action message name, like 'actionlib_tutorials/FibonacciAction' + * * timeout - the timeout length when connecting to the action server + */ +function ActionClient(options) { + var that = this; + options = options || {}; + this.ros = options.ros; + this.serverName = options.serverName; + this.actionName = options.actionName; + this.timeout = options.timeout; + this.omitFeedback = options.omitFeedback; + this.omitStatus = options.omitStatus; + this.omitResult = options.omitResult; + this.goals = {}; + + // flag to check if a status has been received + var receivedStatus = false; + + // create the topics associated with actionlib + this.feedbackListener = new Topic({ + ros : this.ros, + name : this.serverName + '/feedback', + messageType : this.actionName + 'Feedback' + }); + + this.statusListener = new Topic({ + ros : this.ros, + name : this.serverName + '/status', + messageType : 'actionlib_msgs/GoalStatusArray' + }); + + this.resultListener = new Topic({ + ros : this.ros, + name : this.serverName + '/result', + messageType : this.actionName + 'Result' + }); + + this.goalTopic = new Topic({ + ros : this.ros, + name : this.serverName + '/goal', + messageType : this.actionName + 'Goal' + }); + + this.cancelTopic = new Topic({ + ros : this.ros, + name : this.serverName + '/cancel', + messageType : 'actionlib_msgs/GoalID' + }); + + // advertise the goal and cancel topics + this.goalTopic.advertise(); + this.cancelTopic.advertise(); + + // subscribe to the status topic + if (!this.omitStatus) { + this.statusListener.subscribe(function(statusMessage) { + receivedStatus = true; + statusMessage.status_list.forEach(function(status) { + var goal = that.goals[status.goal_id.id]; + if (goal) { + goal.emit('status', status); + } + }); + }); + } + + // subscribe the the feedback topic + if (!this.omitFeedback) { + this.feedbackListener.subscribe(function(feedbackMessage) { + var goal = that.goals[feedbackMessage.status.goal_id.id]; + if (goal) { + goal.emit('status', feedbackMessage.status); + goal.emit('feedback', feedbackMessage.feedback); + } + }); + } + + // subscribe to the result topic + if (!this.omitResult) { + this.resultListener.subscribe(function(resultMessage) { + var goal = that.goals[resultMessage.status.goal_id.id]; + + if (goal) { + goal.emit('status', resultMessage.status); + goal.emit('result', resultMessage.result); + } + }); + } + + // If timeout specified, emit a 'timeout' event if the action server does not respond + if (this.timeout) { + setTimeout(function() { + if (!receivedStatus) { + that.emit('timeout'); + } + }, this.timeout); + } +} + +ActionClient.prototype.__proto__ = EventEmitter2.prototype; + +/** + * Cancel all goals associated with this ActionClient. + */ +ActionClient.prototype.cancel = function() { + var cancelMessage = new Message(); + this.cancelTopic.publish(cancelMessage); +}; + +/** + * Unsubscribe and unadvertise all topics associated with this ActionClient. + */ +ActionClient.prototype.dispose = function() { + this.goalTopic.unadvertise(); + this.cancelTopic.unadvertise(); + if (!this.omitStatus) {this.statusListener.unsubscribe();} + if (!this.omitFeedback) {this.feedbackListener.unsubscribe();} + if (!this.omitResult) {this.resultListener.unsubscribe();} +}; + +module.exports = ActionClient; + +},{"../core/Message":12,"../core/Topic":19,"eventemitter2":2}],8:[function(require,module,exports){ +/** + * @fileOverview + * @author Justin Young - justin@oodar.com.au + * @author Russell Toris - rctoris@wpi.edu + */ + +var Topic = require('../core/Topic'); +var Message = require('../core/Message'); +var EventEmitter2 = require('eventemitter2').EventEmitter2; + +/** + * An actionlib action listener + * + * Emits the following events: + * * 'status' - the status messages received from the action server + * * 'feedback' - the feedback messages received from the action server + * * 'result' - the result returned from the action server + * + * @constructor + * @param options - object with following keys: + * * ros - the ROSLIB.Ros connection handle + * * serverName - the action server name, like /fibonacci + * * actionName - the action message name, like 'actionlib_tutorials/FibonacciAction' + */ +function ActionListener(options) { + var that = this; + options = options || {}; + this.ros = options.ros; + this.serverName = options.serverName; + this.actionName = options.actionName; + this.timeout = options.timeout; + this.omitFeedback = options.omitFeedback; + this.omitStatus = options.omitStatus; + this.omitResult = options.omitResult; + + + // create the topics associated with actionlib + var goalListener = new Topic({ + ros : this.ros, + name : this.serverName + '/goal', + messageType : this.actionName + 'Goal' + }); + + var feedbackListener = new Topic({ + ros : this.ros, + name : this.serverName + '/feedback', + messageType : this.actionName + 'Feedback' + }); + + var statusListener = new Topic({ + ros : this.ros, + name : this.serverName + '/status', + messageType : 'actionlib_msgs/GoalStatusArray' + }); + + var resultListener = new Topic({ + ros : this.ros, + name : this.serverName + '/result', + messageType : this.actionName + 'Result' + }); + + goalListener.subscribe(function(goalMessage) { + that.emit('goal', goalMessage); + }); + + statusListener.subscribe(function(statusMessage) { + statusMessage.status_list.forEach(function(status) { + that.emit('status', status); + }); + }); + + feedbackListener.subscribe(function(feedbackMessage) { + that.emit('status', feedbackMessage.status); + that.emit('feedback', feedbackMessage.feedback); + }); + + // subscribe to the result topic + resultListener.subscribe(function(resultMessage) { + that.emit('status', resultMessage.status); + that.emit('result', resultMessage.result); + }); + +} + +ActionListener.prototype.__proto__ = EventEmitter2.prototype; + +module.exports = ActionListener; + +},{"../core/Message":12,"../core/Topic":19,"eventemitter2":2}],9:[function(require,module,exports){ +/** + * @fileOverview + * @author Russell Toris - rctoris@wpi.edu + */ + +var Message = require('../core/Message'); +var EventEmitter2 = require('eventemitter2').EventEmitter2; + +/** + * An actionlib goal goal is associated with an action server. + * + * Emits the following events: + * * 'timeout' - if a timeout occurred while sending a goal + * + * @constructor + * @param object with following keys: + * * actionClient - the ROSLIB.ActionClient to use with this goal + * * goalMessage - The JSON object containing the goal for the action server + */ +function Goal(options) { + var that = this; + this.actionClient = options.actionClient; + this.goalMessage = options.goalMessage; + this.isFinished = false; + + // Used to create random IDs + var date = new Date(); + + // Create a random ID + this.goalID = 'goal_' + Math.random() + '_' + date.getTime(); + // Fill in the goal message + this.goalMessage = new Message({ + goal_id : { + stamp : { + secs : 0, + nsecs : 0 + }, + id : this.goalID + }, + goal : this.goalMessage + }); + + this.on('status', function(status) { + that.status = status; + }); + + this.on('result', function(result) { + that.isFinished = true; + that.result = result; + }); + + this.on('feedback', function(feedback) { + that.feedback = feedback; + }); + + // Add the goal + this.actionClient.goals[this.goalID] = this; +} + +Goal.prototype.__proto__ = EventEmitter2.prototype; + +/** + * Send the goal to the action server. + * + * @param timeout (optional) - a timeout length for the goal's result + */ +Goal.prototype.send = function(timeout) { + var that = this; + that.actionClient.goalTopic.publish(that.goalMessage); + if (timeout) { + setTimeout(function() { + if (!that.isFinished) { + that.emit('timeout'); + } + }, timeout); + } +}; + +/** + * Cancel the current goal. + */ +Goal.prototype.cancel = function() { + var cancelMessage = new Message({ + id : this.goalID + }); + this.actionClient.cancelTopic.publish(cancelMessage); +}; + +module.exports = Goal; +},{"../core/Message":12,"eventemitter2":2}],10:[function(require,module,exports){ +/** + * @fileOverview + * @author Laura Lindzey - lindzey@gmail.com + */ + +var Topic = require('../core/Topic'); +var Message = require('../core/Message'); +var EventEmitter2 = require('eventemitter2').EventEmitter2; + +/** + * An actionlib action server client. + * + * Emits the following events: + * * 'goal' - goal sent by action client + * * 'cancel' - action client has canceled the request + * + * @constructor + * @param options - object with following keys: + * * ros - the ROSLIB.Ros connection handle + * * serverName - the action server name, like /fibonacci + * * actionName - the action message name, like 'actionlib_tutorials/FibonacciAction' + */ + +function SimpleActionServer(options) { + var that = this; + options = options || {}; + this.ros = options.ros; + this.serverName = options.serverName; + this.actionName = options.actionName; + + // create and advertise publishers + this.feedbackPublisher = new Topic({ + ros : this.ros, + name : this.serverName + '/feedback', + messageType : this.actionName + 'Feedback' + }); + this.feedbackPublisher.advertise(); + + var statusPublisher = new Topic({ + ros : this.ros, + name : this.serverName + '/status', + messageType : 'actionlib_msgs/GoalStatusArray' + }); + statusPublisher.advertise(); + + this.resultPublisher = new Topic({ + ros : this.ros, + name : this.serverName + '/result', + messageType : this.actionName + 'Result' + }); + this.resultPublisher.advertise(); + + // create and subscribe to listeners + var goalListener = new Topic({ + ros : this.ros, + name : this.serverName + '/goal', + messageType : this.actionName + 'Goal' + }); + + var cancelListener = new Topic({ + ros : this.ros, + name : this.serverName + '/cancel', + messageType : 'actionlib_msgs/GoalID' + }); + + // Track the goals and their status in order to publish status... + this.statusMessage = new Message({ + header : { + stamp : {secs : 0, nsecs : 100}, + frame_id : '' + }, + status_list : [] + }); + + // needed for handling preemption prompted by a new goal being received + this.currentGoal = null; // currently tracked goal + this.nextGoal = null; // the one that'll be preempting + + goalListener.subscribe(function(goalMessage) { + + if(that.currentGoal) { + that.nextGoal = goalMessage; + // needs to happen AFTER rest is set up + that.emit('cancel'); + } else { + that.statusMessage.status_list = [{goal_id : goalMessage.goal_id, status : 1}]; + that.currentGoal = goalMessage; + that.emit('goal', goalMessage.goal); + } + }); + + // helper function for determing ordering of timestamps + // returns t1 < t2 + var isEarlier = function(t1, t2) { + if(t1.secs > t2.secs) { + return false; + } else if(t1.secs < t2.secs) { + return true; + } else if(t1.nsecs < t2.nsecs) { + return true; + } else { + return false; + } + }; + + // TODO: this may be more complicated than necessary, since I'm + // not sure if the callbacks can ever wind up with a scenario + // where we've been preempted by a next goal, it hasn't finished + // processing, and then we get a cancel message + cancelListener.subscribe(function(cancelMessage) { + + // cancel ALL goals if both empty + if(cancelMessage.stamp.secs === 0 && cancelMessage.stamp.secs === 0 && cancelMessage.id === '') { + that.nextGoal = null; + if(that.currentGoal) { + that.emit('cancel'); + } + } else { // treat id and stamp independently + if(that.currentGoal && cancelMessage.id === that.currentGoal.goal_id.id) { + that.emit('cancel'); + } else if(that.nextGoal && cancelMessage.id === that.nextGoal.goal_id.id) { + that.nextGoal = null; + } + + if(that.nextGoal && isEarlier(that.nextGoal.goal_id.stamp, + cancelMessage.stamp)) { + that.nextGoal = null; + } + if(that.currentGoal && isEarlier(that.currentGoal.goal_id.stamp, + cancelMessage.stamp)) { + + that.emit('cancel'); + } + } + }); + + // publish status at pseudo-fixed rate; required for clients to know they've connected + var statusInterval = setInterval( function() { + var currentTime = new Date(); + var secs = Math.floor(currentTime.getTime()/1000); + var nsecs = Math.round(1000000000*(currentTime.getTime()/1000-secs)); + that.statusMessage.header.stamp.secs = secs; + that.statusMessage.header.stamp.nsecs = nsecs; + statusPublisher.publish(that.statusMessage); + }, 500); // publish every 500ms + +} + +SimpleActionServer.prototype.__proto__ = EventEmitter2.prototype; + +/** +* Set action state to succeeded and return to client +*/ + +SimpleActionServer.prototype.setSucceeded = function(result2) { + + + var resultMessage = new Message({ + status : {goal_id : this.currentGoal.goal_id, status : 3}, + result : result2 + }); + this.resultPublisher.publish(resultMessage); + + this.statusMessage.status_list = []; + if(this.nextGoal) { + this.currentGoal = this.nextGoal; + this.nextGoal = null; + this.emit('goal', this.currentGoal.goal); + } else { + this.currentGoal = null; + } +}; + +/** +* Function to send feedback +*/ + +SimpleActionServer.prototype.sendFeedback = function(feedback2) { + + var feedbackMessage = new Message({ + status : {goal_id : this.currentGoal.goal_id, status : 1}, + feedback : feedback2 + }); + this.feedbackPublisher.publish(feedbackMessage); +}; + +/** +* Handle case where client requests preemption +*/ + +SimpleActionServer.prototype.setPreempted = function() { + + this.statusMessage.status_list = []; + var resultMessage = new Message({ + status : {goal_id : this.currentGoal.goal_id, status : 2}, + }); + this.resultPublisher.publish(resultMessage); + + if(this.nextGoal) { + this.currentGoal = this.nextGoal; + this.nextGoal = null; + this.emit('goal', this.currentGoal.goal); + } else { + this.currentGoal = null; + } +}; + +module.exports = SimpleActionServer; +},{"../core/Message":12,"../core/Topic":19,"eventemitter2":2}],11:[function(require,module,exports){ +var Ros = require('../core/Ros'); +var mixin = require('../mixin'); + +var action = module.exports = { + ActionClient: require('./ActionClient'), + ActionListener: require('./ActionListener'), + Goal: require('./Goal'), + SimpleActionServer: require('./SimpleActionServer') +}; + +mixin(Ros, ['ActionClient', 'SimpleActionServer'], action); + +},{"../core/Ros":14,"../mixin":26,"./ActionClient":7,"./ActionListener":8,"./Goal":9,"./SimpleActionServer":10}],12:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - baalexander@gmail.com + */ + +var assign = require('object-assign'); + +/** + * Message objects are used for publishing and subscribing to and from topics. + * + * @constructor + * @param values - object matching the fields defined in the .msg definition file + */ +function Message(values) { + assign(this, values); +} + +module.exports = Message; +},{"object-assign":3}],13:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - baalexander@gmail.com + */ + +var Service = require('./Service'); +var ServiceRequest = require('./ServiceRequest'); + +/** + * A ROS parameter. + * + * @constructor + * @param options - possible keys include: + * * ros - the ROSLIB.Ros connection handle + * * name - the param name, like max_vel_x + */ +function Param(options) { + options = options || {}; + this.ros = options.ros; + this.name = options.name; +} + +/** + * Fetches the value of the param. + * + * @param callback - function with the following params: + * * value - the value of the param from ROS. + */ +Param.prototype.get = function(callback) { + var paramClient = new Service({ + ros : this.ros, + name : '/rosapi/get_param', + serviceType : 'rosapi/GetParam' + }); + + var request = new ServiceRequest({ + name : this.name + }); + + paramClient.callService(request, function(result) { + var value = JSON.parse(result.value); + callback(value); + }); +}; + +/** + * Sets the value of the param in ROS. + * + * @param value - value to set param to. + */ +Param.prototype.set = function(value, callback) { + var paramClient = new Service({ + ros : this.ros, + name : '/rosapi/set_param', + serviceType : 'rosapi/SetParam' + }); + + var request = new ServiceRequest({ + name : this.name, + value : JSON.stringify(value) + }); + + paramClient.callService(request, callback); +}; + +/** + * Delete this parameter on the ROS server. + */ +Param.prototype.delete = function(callback) { + var paramClient = new Service({ + ros : this.ros, + name : '/rosapi/delete_param', + serviceType : 'rosapi/DeleteParam' + }); + + var request = new ServiceRequest({ + name : this.name + }); + + paramClient.callService(request, callback); +}; + +module.exports = Param; +},{"./Service":15,"./ServiceRequest":16}],14:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - baalexander@gmail.com + */ + +var WebSocket = require('ws'); +var socketAdapter = require('./SocketAdapter.js'); + +var Service = require('./Service'); +var ServiceRequest = require('./ServiceRequest'); + +var assign = require('object-assign'); +var EventEmitter2 = require('eventemitter2').EventEmitter2; + +/** + * Manages connection to the server and all interactions with ROS. + * + * Emits the following events: + * * 'error' - there was an error with ROS + * * 'connection' - connected to the WebSocket server + * * 'close' - disconnected to the WebSocket server + * * - a message came from rosbridge with the given topic name + * * - a service response came from rosbridge with the given ID + * + * @constructor + * @param options - possible keys include:
+ * * url (optional) - (can be specified later with `connect`) the WebSocket URL for rosbridge or the node server url to connect using socket.io (if socket.io exists in the page)
+ * * groovyCompatibility - don't use interfaces that changed after the last groovy release or rosbridge_suite and related tools (defaults to true) + * * transportLibrary (optional) - one of 'websocket' (default), 'socket.io' or RTCPeerConnection instance controlling how the connection is created in `connect`. + * * transportOptions (optional) - the options to use use when creating a connection. Currently only used if `transportLibrary` is RTCPeerConnection. + */ +function Ros(options) { + options = options || {}; + this.socket = null; + this.idCounter = 0; + this.isConnected = false; + this.transportLibrary = options.transportLibrary || 'websocket'; + this.transportOptions = options.transportOptions || {}; + + if (typeof options.groovyCompatibility === 'undefined') { + this.groovyCompatibility = true; + } + else { + this.groovyCompatibility = options.groovyCompatibility; + } + + // Sets unlimited event listeners. + this.setMaxListeners(0); + + // begin by checking if a URL was given + if (options.url) { + this.connect(options.url); + } +} + +Ros.prototype.__proto__ = EventEmitter2.prototype; + +/** + * Connect to the specified WebSocket. + * + * @param url - WebSocket URL or RTCDataChannel label for Rosbridge + */ +Ros.prototype.connect = function(url) { + if (this.transportLibrary === 'socket.io') { + this.socket = assign(io(url, {'force new connection': true}), socketAdapter(this)); + this.socket.on('connect', this.socket.onopen); + this.socket.on('data', this.socket.onmessage); + this.socket.on('close', this.socket.onclose); + this.socket.on('error', this.socket.onerror); + } else if (this.transportLibrary.constructor.name === 'RTCPeerConnection') { + this.socket = assign(this.transportLibrary.createDataChannel(url, this.transportOptions), socketAdapter(this)); + } else { + if (!this.socket || this.socket.readyState === WebSocket.CLOSED) { + var sock = new WebSocket(url); + sock.binaryType = 'arraybuffer'; + this.socket = assign(sock, socketAdapter(this)); + } + } + +}; + +/** + * Disconnect from the WebSocket server. + */ +Ros.prototype.close = function() { + if (this.socket) { + this.socket.close(); + } +}; + +/** + * Sends an authorization request to the server. + * + * @param mac - MAC (hash) string given by the trusted source. + * @param client - IP of the client. + * @param dest - IP of the destination. + * @param rand - Random string given by the trusted source. + * @param t - Time of the authorization request. + * @param level - User level as a string given by the client. + * @param end - End time of the client's session. + */ +Ros.prototype.authenticate = function(mac, client, dest, rand, t, level, end) { + // create the request + var auth = { + op : 'auth', + mac : mac, + client : client, + dest : dest, + rand : rand, + t : t, + level : level, + end : end + }; + // send the request + this.callOnConnection(auth); +}; + +/** + * Sends the message over the WebSocket, but queues the message up if not yet + * connected. + */ +Ros.prototype.callOnConnection = function(message) { + var that = this; + var messageJson = JSON.stringify(message); + var emitter = null; + if (this.transportLibrary === 'socket.io') { + emitter = function(msg){that.socket.emit('operation', msg);}; + } else { + emitter = function(msg){that.socket.send(msg);}; + } + + if (!this.isConnected) { + that.once('connection', function() { + emitter(messageJson); + }); + } else { + emitter(messageJson); + } +}; + +/** + * Sends a set_level request to the server + * + * @param level - Status level (none, error, warning, info) + * @param id - Optional: Operation ID to change status level on + */ +Ros.prototype.setStatusLevel = function(level, id){ + var levelMsg = { + op: 'set_level', + level: level, + id: id + }; + + this.callOnConnection(levelMsg); +}; + +/** + * Retrieves Action Servers in ROS as an array of string + * + * * actionservers - Array of action server names + */ +Ros.prototype.getActionServers = function(callback, failedCallback) { + var getActionServers = new Service({ + ros : this, + name : '/rosapi/action_servers', + serviceType : 'rosapi/GetActionServers' + }); + + var request = new ServiceRequest({}); + if (typeof failedCallback === 'function'){ + getActionServers.callService(request, + function(result) { + callback(result.action_servers); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + getActionServers.callService(request, function(result) { + callback(result.action_servers); + }); + } +}; + +/** + * Retrieves list of topics in ROS as an array. + * + * @param callback function with params: + * * topics - Array of topic names + */ +Ros.prototype.getTopics = function(callback, failedCallback) { + var topicsClient = new Service({ + ros : this, + name : '/rosapi/topics', + serviceType : 'rosapi/Topics' + }); + + var request = new ServiceRequest(); + if (typeof failedCallback === 'function'){ + topicsClient.callService(request, + function(result) { + callback(result); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + topicsClient.callService(request, function(result) { + callback(result); + }); + } +}; + +/** + * Retrieves Topics in ROS as an array as specific type + * + * @param topicType topic type to find: + * @param callback function with params: + * * topics - Array of topic names + */ +Ros.prototype.getTopicsForType = function(topicType, callback, failedCallback) { + var topicsForTypeClient = new Service({ + ros : this, + name : '/rosapi/topics_for_type', + serviceType : 'rosapi/TopicsForType' + }); + + var request = new ServiceRequest({ + type: topicType + }); + if (typeof failedCallback === 'function'){ + topicsForTypeClient.callService(request, + function(result) { + callback(result.topics); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + topicsForTypeClient.callService(request, function(result) { + callback(result.topics); + }); + } +}; + +/** + * Retrieves list of active service names in ROS. + * + * @param callback - function with the following params: + * * services - array of service names + */ +Ros.prototype.getServices = function(callback, failedCallback) { + var servicesClient = new Service({ + ros : this, + name : '/rosapi/services', + serviceType : 'rosapi/Services' + }); + + var request = new ServiceRequest(); + if (typeof failedCallback === 'function'){ + servicesClient.callService(request, + function(result) { + callback(result.services); + }, + function(message) { + failedCallback(message); + } + ); + }else{ + servicesClient.callService(request, function(result) { + callback(result.services); + }); + } +}; + +/** + * Retrieves list of services in ROS as an array as specific type + * + * @param serviceType service type to find: + * @param callback function with params: + * * topics - Array of service names + */ +Ros.prototype.getServicesForType = function(serviceType, callback, failedCallback) { + var servicesForTypeClient = new Service({ + ros : this, + name : '/rosapi/services_for_type', + serviceType : 'rosapi/ServicesForType' + }); + + var request = new ServiceRequest({ + type: serviceType + }); + if (typeof failedCallback === 'function'){ + servicesForTypeClient.callService(request, + function(result) { + callback(result.services); + }, + function(message) { + failedCallback(message); + } + ); + }else{ + servicesForTypeClient.callService(request, function(result) { + callback(result.services); + }); + } +}; + +/** + * Retrieves a detail of ROS service request. + * + * @param service name of service: + * @param callback - function with params: + * * type - String of the service type + */ +Ros.prototype.getServiceRequestDetails = function(type, callback, failedCallback) { + var serviceTypeClient = new Service({ + ros : this, + name : '/rosapi/service_request_details', + serviceType : 'rosapi/ServiceRequestDetails' + }); + var request = new ServiceRequest({ + type: type + }); + + if (typeof failedCallback === 'function'){ + serviceTypeClient.callService(request, + function(result) { + callback(result); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + serviceTypeClient.callService(request, function(result) { + callback(result); + }); + } +}; + +/** + * Retrieves a detail of ROS service request. + * + * @param service name of service: + * @param callback - function with params: + * * type - String of the service type + */ +Ros.prototype.getServiceResponseDetails = function(type, callback, failedCallback) { + var serviceTypeClient = new Service({ + ros : this, + name : '/rosapi/service_response_details', + serviceType : 'rosapi/ServiceResponseDetails' + }); + var request = new ServiceRequest({ + type: type + }); + + if (typeof failedCallback === 'function'){ + serviceTypeClient.callService(request, + function(result) { + callback(result); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + serviceTypeClient.callService(request, function(result) { + callback(result); + }); + } +}; + +/** + * Retrieves list of active node names in ROS. + * + * @param callback - function with the following params: + * * nodes - array of node names + */ +Ros.prototype.getNodes = function(callback, failedCallback) { + var nodesClient = new Service({ + ros : this, + name : '/rosapi/nodes', + serviceType : 'rosapi/Nodes' + }); + + var request = new ServiceRequest(); + if (typeof failedCallback === 'function'){ + nodesClient.callService(request, + function(result) { + callback(result.nodes); + }, + function(message) { + failedCallback(message); + } + ); + }else{ + nodesClient.callService(request, function(result) { + callback(result.nodes); + }); + } +}; + +/** + * Retrieves list subscribed topics, publishing topics and services of a specific node + * + * @param node name of the node: + * @param callback - function with params: + * * publications - array of published topic names + * * subscriptions - array of subscribed topic names + * * services - array of service names hosted + */ +Ros.prototype.getNodeDetails = function(node, callback, failedCallback) { + var nodesClient = new Service({ + ros : this, + name : '/rosapi/node_details', + serviceType : 'rosapi/NodeDetails' + }); + + var request = new ServiceRequest({ + node: node + }); + if (typeof failedCallback === 'function'){ + nodesClient.callService(request, + function(result) { + callback(result.subscribing, result.publishing, result.services); + }, + function(message) { + failedCallback(message); + } + ); + } else { + nodesClient.callService(request, function(result) { + callback(result); + }); + } +}; + +/** + * Retrieves list of param names from the ROS Parameter Server. + * + * @param callback function with params: + * * params - array of param names. + */ +Ros.prototype.getParams = function(callback, failedCallback) { + var paramsClient = new Service({ + ros : this, + name : '/rosapi/get_param_names', + serviceType : 'rosapi/GetParamNames' + }); + var request = new ServiceRequest(); + if (typeof failedCallback === 'function'){ + paramsClient.callService(request, + function(result) { + callback(result.names); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + paramsClient.callService(request, function(result) { + callback(result.names); + }); + } +}; + +/** + * Retrieves a type of ROS topic. + * + * @param topic name of the topic: + * @param callback - function with params: + * * type - String of the topic type + */ +Ros.prototype.getTopicType = function(topic, callback, failedCallback) { + var topicTypeClient = new Service({ + ros : this, + name : '/rosapi/topic_type', + serviceType : 'rosapi/TopicType' + }); + var request = new ServiceRequest({ + topic: topic + }); + + if (typeof failedCallback === 'function'){ + topicTypeClient.callService(request, + function(result) { + callback(result.type); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + topicTypeClient.callService(request, function(result) { + callback(result.type); + }); + } +}; + +/** + * Retrieves a type of ROS service. + * + * @param service name of service: + * @param callback - function with params: + * * type - String of the service type + */ +Ros.prototype.getServiceType = function(service, callback, failedCallback) { + var serviceTypeClient = new Service({ + ros : this, + name : '/rosapi/service_type', + serviceType : 'rosapi/ServiceType' + }); + var request = new ServiceRequest({ + service: service + }); + + if (typeof failedCallback === 'function'){ + serviceTypeClient.callService(request, + function(result) { + callback(result.type); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + serviceTypeClient.callService(request, function(result) { + callback(result.type); + }); + } +}; + +/** + * Retrieves a detail of ROS message. + * + * @param callback - function with params: + * * details - Array of the message detail + * @param message - String of a topic type + */ +Ros.prototype.getMessageDetails = function(message, callback, failedCallback) { + var messageDetailClient = new Service({ + ros : this, + name : '/rosapi/message_details', + serviceType : 'rosapi/MessageDetails' + }); + var request = new ServiceRequest({ + type: message + }); + + if (typeof failedCallback === 'function'){ + messageDetailClient.callService(request, + function(result) { + callback(result.typedefs); + }, + function(message){ + failedCallback(message); + } + ); + }else{ + messageDetailClient.callService(request, function(result) { + callback(result.typedefs); + }); + } +}; + +/** + * Decode a typedefs into a dictionary like `rosmsg show foo/bar` + * + * @param defs - array of type_def dictionary + */ +Ros.prototype.decodeTypeDefs = function(defs) { + var that = this; + + // calls itself recursively to resolve type definition using hints. + var decodeTypeDefsRec = function(theType, hints) { + var typeDefDict = {}; + for (var i = 0; i < theType.fieldnames.length; i++) { + var arrayLen = theType.fieldarraylen[i]; + var fieldName = theType.fieldnames[i]; + var fieldType = theType.fieldtypes[i]; + if (fieldType.indexOf('/') === -1) { // check the fieldType includes '/' or not + if (arrayLen === -1) { + typeDefDict[fieldName] = fieldType; + } + else { + typeDefDict[fieldName] = [fieldType]; + } + } + else { + // lookup the name + var sub = false; + for (var j = 0; j < hints.length; j++) { + if (hints[j].type.toString() === fieldType.toString()) { + sub = hints[j]; + break; + } + } + if (sub) { + var subResult = decodeTypeDefsRec(sub, hints); + if (arrayLen === -1) { + } + else { + typeDefDict[fieldName] = [subResult]; + } + } + else { + that.emit('error', 'Cannot find ' + fieldType + ' in decodeTypeDefs'); + } + } + } + return typeDefDict; + }; + + return decodeTypeDefsRec(defs[0], defs); +}; + + +module.exports = Ros; + +},{"./Service":15,"./ServiceRequest":16,"./SocketAdapter.js":18,"eventemitter2":2,"object-assign":3,"ws":42}],15:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - baalexander@gmail.com + */ + +var ServiceResponse = require('./ServiceResponse'); +var ServiceRequest = require('./ServiceRequest'); +var EventEmitter2 = require('eventemitter2').EventEmitter2; + +/** + * A ROS service client. + * + * @constructor + * @params options - possible keys include: + * * ros - the ROSLIB.Ros connection handle + * * name - the service name, like /add_two_ints + * * serviceType - the service type, like 'rospy_tutorials/AddTwoInts' + */ +function Service(options) { + options = options || {}; + this.ros = options.ros; + this.name = options.name; + this.serviceType = options.serviceType; + this.isAdvertised = false; + + this._serviceCallback = null; +} +Service.prototype.__proto__ = EventEmitter2.prototype; +/** + * Calls the service. Returns the service response in the + * callback. Does nothing if this service is currently advertised. + * + * @param request - the ROSLIB.ServiceRequest to send + * @param callback - function with params: + * * response - the response from the service request + * @param failedCallback - the callback function when the service call failed (optional). Params: + * * error - the error message reported by ROS + */ +Service.prototype.callService = function(request, callback, failedCallback) { + if (this.isAdvertised) { + return; + } + + var serviceCallId = 'call_service:' + this.name + ':' + (++this.ros.idCounter); + + if (callback || failedCallback) { + this.ros.once(serviceCallId, function(message) { + if (message.result !== undefined && message.result === false) { + if (typeof failedCallback === 'function') { + failedCallback(message.values); + } + } else if (typeof callback === 'function') { + callback(new ServiceResponse(message.values)); + } + }); + } + + var call = { + op : 'call_service', + id : serviceCallId, + service : this.name, + type: this.serviceType, + args : request + }; + this.ros.callOnConnection(call); +}; + +/** + * Advertise the service. This turns the Service object from a client + * into a server. The callback will be called with every request + * that's made on this service. + * + * @param callback - This works similarly to the callback for a C++ service and should take the following params: + * * request - the service request + * * response - an empty dictionary. Take care not to overwrite this. Instead, only modify the values within. + * It should return true if the service has finished successfully, + * i.e. without any fatal errors. + */ +Service.prototype.advertise = function(callback) { + if (this.isAdvertised || typeof callback !== 'function') { + return; + } + + this._serviceCallback = callback; + this.ros.on(this.name, this._serviceResponse.bind(this)); + this.ros.callOnConnection({ + op: 'advertise_service', + type: this.serviceType, + service: this.name + }); + this.isAdvertised = true; +}; + +Service.prototype.unadvertise = function() { + if (!this.isAdvertised) { + return; + } + this.ros.callOnConnection({ + op: 'unadvertise_service', + service: this.name + }); + this.isAdvertised = false; +}; + +Service.prototype._serviceResponse = function(rosbridgeRequest) { + var response = {}; + var success = this._serviceCallback(rosbridgeRequest.args, response); + + var call = { + op: 'service_response', + service: this.name, + values: new ServiceResponse(response), + result: success + }; + + if (rosbridgeRequest.id) { + call.id = rosbridgeRequest.id; + } + + this.ros.callOnConnection(call); +}; + +module.exports = Service; + +},{"./ServiceRequest":16,"./ServiceResponse":17,"eventemitter2":2}],16:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - balexander@willowgarage.com + */ + +var assign = require('object-assign'); + +/** + * A ServiceRequest is passed into the service call. + * + * @constructor + * @param values - object matching the fields defined in the .srv definition file + */ +function ServiceRequest(values) { + assign(this, values); +} + +module.exports = ServiceRequest; +},{"object-assign":3}],17:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - balexander@willowgarage.com + */ + +var assign = require('object-assign'); + +/** + * A ServiceResponse is returned from the service call. + * + * @constructor + * @param values - object matching the fields defined in the .srv definition file + */ +function ServiceResponse(values) { + assign(this, values); +} + +module.exports = ServiceResponse; +},{"object-assign":3}],18:[function(require,module,exports){ +/** + * Socket event handling utilities for handling events on either + * WebSocket and TCP sockets + * + * Note to anyone reviewing this code: these functions are called + * in the context of their parent object, unless bound + * @fileOverview + */ +'use strict'; + +var decompressPng = require('../util/decompressPng'); +var CBOR = require('cbor-js'); +var typedArrayTagger = require('../util/cborTypedArrayTags'); +var WebSocket = require('ws'); +var BSON = null; +if(typeof bson !== 'undefined'){ + BSON = bson().BSON; +} + +/** + * Events listeners for a WebSocket or TCP socket to a JavaScript + * ROS Client. Sets up Messages for a given topic to trigger an + * event on the ROS client. + * + * @namespace SocketAdapter + * @private + */ +function SocketAdapter(client) { + function handleMessage(message) { + if (message.op === 'publish') { + client.emit(message.topic, message.msg); + } else if (message.op === 'service_response') { + client.emit(message.id, message); + } else if (message.op === 'call_service') { + client.emit(message.service, message); + } else if(message.op === 'status'){ + if(message.id){ + client.emit('status:'+message.id, message); + } else { + client.emit('status', message); + } + } + } + + function handlePng(message, callback) { + if (message.op === 'png') { + decompressPng(message.data, callback); + } else { + callback(message); + } + } + + function decodeBSON(data, callback) { + if (!BSON) { + throw 'Cannot process BSON encoded message without BSON header.'; + } + var reader = new FileReader(); + reader.onload = function() { + var uint8Array = new Uint8Array(this.result); + var msg = BSON.deserialize(uint8Array); + callback(msg); + }; + reader.readAsArrayBuffer(data); + } + + return { + /** + * Emits a 'connection' event on WebSocket connection. + * + * @param event - the argument to emit with the event. + * @memberof SocketAdapter + */ + onopen: function onOpen(event) { + client.isConnected = true; + client.emit('connection', event); + }, + + /** + * Emits a 'close' event on WebSocket disconnection. + * + * @param event - the argument to emit with the event. + * @memberof SocketAdapter + */ + onclose: function onClose(event) { + client.isConnected = false; + client.emit('close', event); + }, + + /** + * Emits an 'error' event whenever there was an error. + * + * @param event - the argument to emit with the event. + * @memberof SocketAdapter + */ + onerror: function onError(event) { + client.emit('error', event); + }, + + /** + * Parses message responses from rosbridge and sends to the appropriate + * topic, service, or param. + * + * @param message - the raw JSON message from rosbridge. + * @memberof SocketAdapter + */ + onmessage: function onMessage(data) { + if (typeof Blob !== 'undefined' && data.data instanceof Blob) { + decodeBSON(data.data, function (message) { + handlePng(message, handleMessage); + }); + } else if (data.data instanceof ArrayBuffer) { + var decoded = CBOR.decode(data.data, typedArrayTagger); + handleMessage(decoded); + } else { + var message = JSON.parse(typeof data === 'string' ? data : data.data); + handlePng(message, handleMessage); + } + } + }; +} + +module.exports = SocketAdapter; + +},{"../util/cborTypedArrayTags":41,"../util/decompressPng":44,"cbor-js":1,"ws":42}],19:[function(require,module,exports){ +/** + * @fileoverview + * @author Brandon Alexander - baalexander@gmail.com + */ + +var EventEmitter2 = require('eventemitter2').EventEmitter2; +var Message = require('./Message'); + +/** + * Publish and/or subscribe to a topic in ROS. + * + * Emits the following events: + * * 'warning' - if there are any warning during the Topic creation + * * 'message' - the message data from rosbridge + * + * @constructor + * @param options - object with following keys: + * * ros - the ROSLIB.Ros connection handle + * * name - the topic name, like /cmd_vel + * * messageType - the message type, like 'std_msgs/String' + * * compression - the type of compression to use, like 'png' or 'cbor' + * * throttle_rate - the rate (in ms in between messages) at which to throttle the topics + * * queue_size - the queue created at bridge side for re-publishing webtopics (defaults to 100) + * * latch - latch the topic when publishing + * * queue_length - the queue length at bridge side used when subscribing (defaults to 0, no queueing). + * * reconnect_on_close - the flag to enable resubscription and readvertisement on close event(defaults to true). + */ +function Topic(options) { + options = options || {}; + this.ros = options.ros; + this.name = options.name; + this.messageType = options.messageType; + this.isAdvertised = false; + this.compression = options.compression || 'none'; + this.throttle_rate = options.throttle_rate || 0; + this.latch = options.latch || false; + this.queue_size = options.queue_size || 100; + this.queue_length = options.queue_length || 0; + this.reconnect_on_close = options.reconnect_on_close || true; + + // Check for valid compression types + if (this.compression && this.compression !== 'png' && + this.compression !== 'cbor' && this.compression !== 'none') { + this.emit('warning', this.compression + + ' compression is not supported. No compression will be used.'); + this.compression = 'none'; + } + + // Check if throttle rate is negative + if (this.throttle_rate < 0) { + this.emit('warning', this.throttle_rate + ' is not allowed. Set to 0'); + this.throttle_rate = 0; + } + + var that = this; + if (this.reconnect_on_close) { + this.callForSubscribeAndAdvertise = function(message) { + that.ros.callOnConnection(message); + + that.waitForReconnect = false; + that.reconnectFunc = function() { + if(!that.waitForReconnect) { + that.waitForReconnect = true; + that.ros.callOnConnection(message); + that.ros.once('connection', function() { + that.waitForReconnect = false; + }); + } + }; + that.ros.on('close', that.reconnectFunc); + }; + } + else { + this.callForSubscribeAndAdvertise = this.ros.callOnConnection; + } + + this._messageCallback = function(data) { + that.emit('message', new Message(data)); + }; +} +Topic.prototype.__proto__ = EventEmitter2.prototype; + +/** + * Every time a message is published for the given topic, the callback + * will be called with the message object. + * + * @param callback - function with the following params: + * * message - the published message + */ +Topic.prototype.subscribe = function(callback) { + if (typeof callback === 'function') { + this.on('message', callback); + } + + if (this.subscribeId) { return; } + this.ros.on(this.name, this._messageCallback); + this.subscribeId = 'subscribe:' + this.name + ':' + (++this.ros.idCounter); + + this.callForSubscribeAndAdvertise({ + op: 'subscribe', + id: this.subscribeId, + type: this.messageType, + topic: this.name, + compression: this.compression, + throttle_rate: this.throttle_rate, + queue_length: this.queue_length + }); +}; + +/** + * Unregisters as a subscriber for the topic. Unsubscribing stop remove + * all subscribe callbacks. To remove a call back, you must explicitly + * pass the callback function in. + * + * @param callback - the optional callback to unregister, if + * * provided and other listeners are registered the topic won't + * * unsubscribe, just stop emitting to the passed listener + */ +Topic.prototype.unsubscribe = function(callback) { + if (callback) { + this.off('message', callback); + // If there is any other callbacks still subscribed don't unsubscribe + if (this.listeners('message').length) { return; } + } + if (!this.subscribeId) { return; } + // Note: Don't call this.removeAllListeners, allow client to handle that themselves + this.ros.off(this.name, this._messageCallback); + if(this.reconnect_on_close) { + this.ros.off('close', this.reconnectFunc); + } + this.emit('unsubscribe'); + this.ros.callOnConnection({ + op: 'unsubscribe', + id: this.subscribeId, + topic: this.name + }); + this.subscribeId = null; +}; + + +/** + * Registers as a publisher for the topic. + */ +Topic.prototype.advertise = function() { + if (this.isAdvertised) { + return; + } + this.advertiseId = 'advertise:' + this.name + ':' + (++this.ros.idCounter); + this.callForSubscribeAndAdvertise({ + op: 'advertise', + id: this.advertiseId, + type: this.messageType, + topic: this.name, + latch: this.latch, + queue_size: this.queue_size + }); + this.isAdvertised = true; + + if(!this.reconnect_on_close) { + var that = this; + this.ros.on('close', function() { + that.isAdvertised = false; + }); + } +}; + +/** + * Unregisters as a publisher for the topic. + */ +Topic.prototype.unadvertise = function() { + if (!this.isAdvertised) { + return; + } + if(this.reconnect_on_close) { + this.ros.off('close', this.reconnectFunc); + } + this.emit('unadvertise'); + this.ros.callOnConnection({ + op: 'unadvertise', + id: this.advertiseId, + topic: this.name + }); + this.isAdvertised = false; +}; + +/** + * Publish the message. + * + * @param message - A ROSLIB.Message object. + */ +Topic.prototype.publish = function(message) { + if (!this.isAdvertised) { + this.advertise(); + } + + this.ros.idCounter++; + var call = { + op: 'publish', + id: 'publish:' + this.name + ':' + this.ros.idCounter, + topic: this.name, + msg: message, + latch: this.latch + }; + this.ros.callOnConnection(call); +}; + +module.exports = Topic; + +},{"./Message":12,"eventemitter2":2}],20:[function(require,module,exports){ +var mixin = require('../mixin'); + +var core = module.exports = { + Ros: require('./Ros'), + Topic: require('./Topic'), + Message: require('./Message'), + Param: require('./Param'), + Service: require('./Service'), + ServiceRequest: require('./ServiceRequest'), + ServiceResponse: require('./ServiceResponse') +}; + +mixin(core.Ros, ['Param', 'Service', 'Topic'], core); + +},{"../mixin":26,"./Message":12,"./Param":13,"./Ros":14,"./Service":15,"./ServiceRequest":16,"./ServiceResponse":17,"./Topic":19}],21:[function(require,module,exports){ +/** + * @fileoverview + * @author David Gossow - dgossow@willowgarage.com + */ + +var Vector3 = require('./Vector3'); +var Quaternion = require('./Quaternion'); + +/** + * A Pose in 3D space. Values are copied into this object. + * + * @constructor + * @param options - object with following keys: + * * position - the Vector3 describing the position + * * orientation - the ROSLIB.Quaternion describing the orientation + */ +function Pose(options) { + options = options || {}; + // copy the values into this object if they exist + this.position = new Vector3(options.position); + this.orientation = new Quaternion(options.orientation); +} + +/** + * Apply a transform against this pose. + * + * @param tf the transform + */ +Pose.prototype.applyTransform = function(tf) { + this.position.multiplyQuaternion(tf.rotation); + this.position.add(tf.translation); + var tmp = tf.rotation.clone(); + tmp.multiply(this.orientation); + this.orientation = tmp; +}; + +/** + * Clone a copy of this pose. + * + * @returns the cloned pose + */ +Pose.prototype.clone = function() { + return new Pose(this); +}; + +/** + * Multiplies this pose with another pose without altering this pose. + * + * @returns Result of multiplication. + */ +Pose.prototype.multiply = function(pose) { + var p = pose.clone(); + p.applyTransform({ rotation: this.orientation, translation: this.position }); + return p; +}; + +/** + * Computes the inverse of this pose. + * + * @returns Inverse of pose. + */ +Pose.prototype.getInverse = function() { + var inverse = this.clone(); + inverse.orientation.invert(); + inverse.position.multiplyQuaternion(inverse.orientation); + inverse.position.x *= -1; + inverse.position.y *= -1; + inverse.position.z *= -1; + return inverse; +}; + +module.exports = Pose; +},{"./Quaternion":22,"./Vector3":24}],22:[function(require,module,exports){ +/** + * @fileoverview + * @author David Gossow - dgossow@willowgarage.com + */ + +/** + * A Quaternion. + * + * @constructor + * @param options - object with following keys: + * * x - the x value + * * y - the y value + * * z - the z value + * * w - the w value + */ +function Quaternion(options) { + options = options || {}; + this.x = options.x || 0; + this.y = options.y || 0; + this.z = options.z || 0; + this.w = (typeof options.w === 'number') ? options.w : 1; +} + +/** + * Perform a conjugation on this quaternion. + */ +Quaternion.prototype.conjugate = function() { + this.x *= -1; + this.y *= -1; + this.z *= -1; +}; + +/** + * Return the norm of this quaternion. + */ +Quaternion.prototype.norm = function() { + return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w); +}; + +/** + * Perform a normalization on this quaternion. + */ +Quaternion.prototype.normalize = function() { + var l = Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w); + if (l === 0) { + this.x = 0; + this.y = 0; + this.z = 0; + this.w = 1; + } else { + l = 1 / l; + this.x = this.x * l; + this.y = this.y * l; + this.z = this.z * l; + this.w = this.w * l; + } +}; + +/** + * Convert this quaternion into its inverse. + */ +Quaternion.prototype.invert = function() { + this.conjugate(); + this.normalize(); +}; + +/** + * Set the values of this quaternion to the product of itself and the given quaternion. + * + * @param q the quaternion to multiply with + */ +Quaternion.prototype.multiply = function(q) { + var newX = this.x * q.w + this.y * q.z - this.z * q.y + this.w * q.x; + var newY = -this.x * q.z + this.y * q.w + this.z * q.x + this.w * q.y; + var newZ = this.x * q.y - this.y * q.x + this.z * q.w + this.w * q.z; + var newW = -this.x * q.x - this.y * q.y - this.z * q.z + this.w * q.w; + this.x = newX; + this.y = newY; + this.z = newZ; + this.w = newW; +}; + +/** + * Clone a copy of this quaternion. + * + * @returns the cloned quaternion + */ +Quaternion.prototype.clone = function() { + return new Quaternion(this); +}; + +module.exports = Quaternion; + +},{}],23:[function(require,module,exports){ +/** + * @fileoverview + * @author David Gossow - dgossow@willowgarage.com + */ + +var Vector3 = require('./Vector3'); +var Quaternion = require('./Quaternion'); + +/** + * A Transform in 3-space. Values are copied into this object. + * + * @constructor + * @param options - object with following keys: + * * translation - the Vector3 describing the translation + * * rotation - the ROSLIB.Quaternion describing the rotation + */ +function Transform(options) { + options = options || {}; + // Copy the values into this object if they exist + this.translation = new Vector3(options.translation); + this.rotation = new Quaternion(options.rotation); +} + +/** + * Clone a copy of this transform. + * + * @returns the cloned transform + */ +Transform.prototype.clone = function() { + return new Transform(this); +}; + +module.exports = Transform; +},{"./Quaternion":22,"./Vector3":24}],24:[function(require,module,exports){ +/** + * @fileoverview + * @author David Gossow - dgossow@willowgarage.com + */ + +/** + * A 3D vector. + * + * @constructor + * @param options - object with following keys: + * * x - the x value + * * y - the y value + * * z - the z value + */ +function Vector3(options) { + options = options || {}; + this.x = options.x || 0; + this.y = options.y || 0; + this.z = options.z || 0; +} + +/** + * Set the values of this vector to the sum of itself and the given vector. + * + * @param v the vector to add with + */ +Vector3.prototype.add = function(v) { + this.x += v.x; + this.y += v.y; + this.z += v.z; +}; + +/** + * Set the values of this vector to the difference of itself and the given vector. + * + * @param v the vector to subtract with + */ +Vector3.prototype.subtract = function(v) { + this.x -= v.x; + this.y -= v.y; + this.z -= v.z; +}; + +/** + * Multiply the given Quaternion with this vector. + * + * @param q - the quaternion to multiply with + */ +Vector3.prototype.multiplyQuaternion = function(q) { + var ix = q.w * this.x + q.y * this.z - q.z * this.y; + var iy = q.w * this.y + q.z * this.x - q.x * this.z; + var iz = q.w * this.z + q.x * this.y - q.y * this.x; + var iw = -q.x * this.x - q.y * this.y - q.z * this.z; + this.x = ix * q.w + iw * -q.x + iy * -q.z - iz * -q.y; + this.y = iy * q.w + iw * -q.y + iz * -q.x - ix * -q.z; + this.z = iz * q.w + iw * -q.z + ix * -q.y - iy * -q.x; +}; + +/** + * Clone a copy of this vector. + * + * @returns the cloned vector + */ +Vector3.prototype.clone = function() { + return new Vector3(this); +}; + +module.exports = Vector3; +},{}],25:[function(require,module,exports){ +module.exports = { + Pose: require('./Pose'), + Quaternion: require('./Quaternion'), + Transform: require('./Transform'), + Vector3: require('./Vector3') +}; + +},{"./Pose":21,"./Quaternion":22,"./Transform":23,"./Vector3":24}],26:[function(require,module,exports){ +/** + * Mixin a feature to the core/Ros prototype. + * For example, mixin(Ros, ['Topic'], {Topic: }) + * will add a topic bound to any Ros instances so a user + * can call `var topic = ros.Topic({name: '/foo'});` + * + * @author Graeme Yeates - github.com/megawac + */ +module.exports = function(Ros, classes, features) { + classes.forEach(function(className) { + var Class = features[className]; + Ros.prototype[className] = function(options) { + options.ros = this; + return new Class(options); + }; + }); +}; + +},{}],27:[function(require,module,exports){ +/** + * @fileoverview + * @author David Gossow - dgossow@willowgarage.com + */ + +var ActionClient = require('../actionlib/ActionClient'); +var Goal = require('../actionlib/Goal'); + +var Service = require('../core/Service.js'); +var ServiceRequest = require('../core/ServiceRequest.js'); + +var Transform = require('../math/Transform'); + +/** + * A TF Client that listens to TFs from tf2_web_republisher. + * + * @constructor + * @param options - object with following keys: + * * ros - the ROSLIB.Ros connection handle + * * fixedFrame - the fixed frame, like /base_link + * * angularThres - the angular threshold for the TF republisher + * * transThres - the translation threshold for the TF republisher + * * rate - the rate for the TF republisher + * * updateDelay - the time (in ms) to wait after a new subscription + * to update the TF republisher's list of TFs + * * topicTimeout - the timeout parameter for the TF republisher + * * serverName (optional) - the name of the tf2_web_republisher server + * * repubServiceName (optional) - the name of the republish_tfs service (non groovy compatibility mode only) + * default: '/republish_tfs' + */ +function TFClient(options) { + options = options || {}; + this.ros = options.ros; + this.fixedFrame = options.fixedFrame || '/base_link'; + this.angularThres = options.angularThres || 2.0; + this.transThres = options.transThres || 0.01; + this.rate = options.rate || 10.0; + this.updateDelay = options.updateDelay || 50; + var seconds = options.topicTimeout || 2.0; + var secs = Math.floor(seconds); + var nsecs = Math.floor((seconds - secs) * 1000000000); + this.topicTimeout = { + secs: secs, + nsecs: nsecs + }; + this.serverName = options.serverName || '/tf2_web_republisher'; + this.repubServiceName = options.repubServiceName || '/republish_tfs'; + + this.currentGoal = false; + this.currentTopic = false; + this.frameInfos = {}; + this.republisherUpdateRequested = false; + + // Create an Action client + this.actionClient = this.ros.ActionClient({ + serverName : this.serverName, + actionName : 'tf2_web_republisher/TFSubscriptionAction', + omitStatus : true, + omitResult : true + }); + + // Create a Service client + this.serviceClient = this.ros.Service({ + name: this.repubServiceName, + serviceType: 'tf2_web_republisher/RepublishTFs' + }); +} + +/** + * Process the incoming TF message and send them out using the callback + * functions. + * + * @param tf - the TF message from the server + */ +TFClient.prototype.processTFArray = function(tf) { + var that = this; + tf.transforms.forEach(function(transform) { + var frameID = transform.child_frame_id; + if (frameID[0] === '/') + { + frameID = frameID.substring(1); + } + var info = this.frameInfos[frameID]; + if (info) { + info.transform = new Transform({ + translation : transform.transform.translation, + rotation : transform.transform.rotation + }); + info.cbs.forEach(function(cb) { + cb(info.transform); + }); + } + }, this); +}; + +/** + * Create and send a new goal (or service request) to the tf2_web_republisher + * based on the current list of TFs. + */ +TFClient.prototype.updateGoal = function() { + var goalMessage = { + source_frames : Object.keys(this.frameInfos), + target_frame : this.fixedFrame, + angular_thres : this.angularThres, + trans_thres : this.transThres, + rate : this.rate + }; + + // if we're running in groovy compatibility mode (the default) + // then use the action interface to tf2_web_republisher + if(this.ros.groovyCompatibility) { + if (this.currentGoal) { + this.currentGoal.cancel(); + } + this.currentGoal = new Goal({ + actionClient : this.actionClient, + goalMessage : goalMessage + }); + + this.currentGoal.on('feedback', this.processTFArray.bind(this)); + this.currentGoal.send(); + } + else { + // otherwise, use the service interface + // The service interface has the same parameters as the action, + // plus the timeout + goalMessage.timeout = this.topicTimeout; + var request = new ServiceRequest(goalMessage); + + this.serviceClient.callService(request, this.processResponse.bind(this)); + } + + this.republisherUpdateRequested = false; +}; + +/** + * Process the service response and subscribe to the tf republisher + * topic + * + * @param response the service response containing the topic name + */ +TFClient.prototype.processResponse = function(response) { + // if we subscribed to a topic before, unsubscribe so + // the republisher stops publishing it + if (this.currentTopic) { + this.currentTopic.unsubscribe(); + } + + this.currentTopic = this.ros.Topic({ + name: response.topic_name, + messageType: 'tf2_web_republisher/TFArray' + }); + this.currentTopic.subscribe(this.processTFArray.bind(this)); +}; + +/** + * Subscribe to the given TF frame. + * + * @param frameID - the TF frame to subscribe to + * @param callback - function with params: + * * transform - the transform data + */ +TFClient.prototype.subscribe = function(frameID, callback) { + // remove leading slash, if it's there + if (frameID[0] === '/') + { + frameID = frameID.substring(1); + } + // if there is no callback registered for the given frame, create emtpy callback list + if (!this.frameInfos[frameID]) { + this.frameInfos[frameID] = { + cbs: [] + }; + if (!this.republisherUpdateRequested) { + setTimeout(this.updateGoal.bind(this), this.updateDelay); + this.republisherUpdateRequested = true; + } + } + // if we already have a transform, call back immediately + else if (this.frameInfos[frameID].transform) { + callback(this.frameInfos[frameID].transform); + } + this.frameInfos[frameID].cbs.push(callback); +}; + +/** + * Unsubscribe from the given TF frame. + * + * @param frameID - the TF frame to unsubscribe from + * @param callback - the callback function to remove + */ +TFClient.prototype.unsubscribe = function(frameID, callback) { + // remove leading slash, if it's there + if (frameID[0] === '/') + { + frameID = frameID.substring(1); + } + var info = this.frameInfos[frameID]; + for (var cbs = info && info.cbs || [], idx = cbs.length; idx--;) { + if (cbs[idx] === callback) { + cbs.splice(idx, 1); + } + } + if (!callback || cbs.length === 0) { + delete this.frameInfos[frameID]; + } +}; + +/** + * Unsubscribe and unadvertise all topics associated with this TFClient. + */ +TFClient.prototype.dispose = function() { + this.actionClient.dispose(); + if (this.currentTopic) { + this.currentTopic.unsubscribe(); + } +}; + +module.exports = TFClient; + +},{"../actionlib/ActionClient":7,"../actionlib/Goal":9,"../core/Service.js":15,"../core/ServiceRequest.js":16,"../math/Transform":23}],28:[function(require,module,exports){ +var Ros = require('../core/Ros'); +var mixin = require('../mixin'); + +var tf = module.exports = { + TFClient: require('./TFClient') +}; + +mixin(Ros, ['TFClient'], tf); +},{"../core/Ros":14,"../mixin":26,"./TFClient":27}],29:[function(require,module,exports){ +/** + * @fileOverview + * @author Benjamin Pitzer - ben.pitzer@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +var Vector3 = require('../math/Vector3'); +var UrdfTypes = require('./UrdfTypes'); + +/** + * A Box element in a URDF. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + */ +function UrdfBox(options) { + this.dimension = null; + this.type = UrdfTypes.URDF_BOX; + + // Parse the xml string + var xyz = options.xml.getAttribute('size').split(' '); + this.dimension = new Vector3({ + x : parseFloat(xyz[0]), + y : parseFloat(xyz[1]), + z : parseFloat(xyz[2]) + }); +} + +module.exports = UrdfBox; +},{"../math/Vector3":24,"./UrdfTypes":38}],30:[function(require,module,exports){ +/** + * @fileOverview + * @author Benjamin Pitzer - ben.pitzer@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +/** + * A Color element in a URDF. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + */ +function UrdfColor(options) { + // Parse the xml string + var rgba = options.xml.getAttribute('rgba').split(' '); + this.r = parseFloat(rgba[0]); + this.g = parseFloat(rgba[1]); + this.b = parseFloat(rgba[2]); + this.a = parseFloat(rgba[3]); +} + +module.exports = UrdfColor; +},{}],31:[function(require,module,exports){ +/** + * @fileOverview + * @author Benjamin Pitzer - ben.pitzer@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +var UrdfTypes = require('./UrdfTypes'); + +/** + * A Cylinder element in a URDF. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + */ +function UrdfCylinder(options) { + this.type = UrdfTypes.URDF_CYLINDER; + this.length = parseFloat(options.xml.getAttribute('length')); + this.radius = parseFloat(options.xml.getAttribute('radius')); +} + +module.exports = UrdfCylinder; +},{"./UrdfTypes":38}],32:[function(require,module,exports){ +/** + * @fileOverview + * @author David V. Lu!! davidvlu@gmail.com + */ + +var Pose = require('../math/Pose'); +var Vector3 = require('../math/Vector3'); +var Quaternion = require('../math/Quaternion'); + +/** + * A Joint element in a URDF. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + */ +function UrdfJoint(options) { + this.name = options.xml.getAttribute('name'); + this.type = options.xml.getAttribute('type'); + + var parents = options.xml.getElementsByTagName('parent'); + if(parents.length > 0) { + this.parent = parents[0].getAttribute('link'); + } + + var children = options.xml.getElementsByTagName('child'); + if(children.length > 0) { + this.child = children[0].getAttribute('link'); + } + + var limits = options.xml.getElementsByTagName('limit'); + if (limits.length > 0) { + this.minval = parseFloat( limits[0].getAttribute('lower') ); + this.maxval = parseFloat( limits[0].getAttribute('upper') ); + } + + // Origin + var origins = options.xml.getElementsByTagName('origin'); + if (origins.length === 0) { + // use the identity as the default + this.origin = new Pose(); + } else { + // Check the XYZ + var xyz = origins[0].getAttribute('xyz'); + var position = new Vector3(); + if (xyz) { + xyz = xyz.split(' '); + position = new Vector3({ + x : parseFloat(xyz[0]), + y : parseFloat(xyz[1]), + z : parseFloat(xyz[2]) + }); + } + + // Check the RPY + var rpy = origins[0].getAttribute('rpy'); + var orientation = new Quaternion(); + if (rpy) { + rpy = rpy.split(' '); + // Convert from RPY + var roll = parseFloat(rpy[0]); + var pitch = parseFloat(rpy[1]); + var yaw = parseFloat(rpy[2]); + var phi = roll / 2.0; + var the = pitch / 2.0; + var psi = yaw / 2.0; + var x = Math.sin(phi) * Math.cos(the) * Math.cos(psi) - Math.cos(phi) * Math.sin(the) + * Math.sin(psi); + var y = Math.cos(phi) * Math.sin(the) * Math.cos(psi) + Math.sin(phi) * Math.cos(the) + * Math.sin(psi); + var z = Math.cos(phi) * Math.cos(the) * Math.sin(psi) - Math.sin(phi) * Math.sin(the) + * Math.cos(psi); + var w = Math.cos(phi) * Math.cos(the) * Math.cos(psi) + Math.sin(phi) * Math.sin(the) + * Math.sin(psi); + + orientation = new Quaternion({ + x : x, + y : y, + z : z, + w : w + }); + orientation.normalize(); + } + this.origin = new Pose({ + position : position, + orientation : orientation + }); + } +} + +module.exports = UrdfJoint; + +},{"../math/Pose":21,"../math/Quaternion":22,"../math/Vector3":24}],33:[function(require,module,exports){ +/** + * @fileOverview + * @author Benjamin Pitzer - ben.pitzer@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +var UrdfVisual = require('./UrdfVisual'); + +/** + * A Link element in a URDF. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + */ +function UrdfLink(options) { + this.name = options.xml.getAttribute('name'); + this.visuals = []; + var visuals = options.xml.getElementsByTagName('visual'); + + for( var i=0; i 0) { + this.textureFilename = textures[0].getAttribute('filename'); + } + + // Color + var colors = options.xml.getElementsByTagName('color'); + if (colors.length > 0) { + // Parse the RBGA string + this.color = new UrdfColor({ + xml : colors[0] + }); + } +} + +UrdfMaterial.prototype.isLink = function() { + return this.color === null && this.textureFilename === null; +}; + +var assign = require('object-assign'); + +UrdfMaterial.prototype.assign = function(obj) { + return assign(this, obj); +}; + +module.exports = UrdfMaterial; + +},{"./UrdfColor":30,"object-assign":3}],35:[function(require,module,exports){ +/** + * @fileOverview + * @author Benjamin Pitzer - ben.pitzer@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +var Vector3 = require('../math/Vector3'); +var UrdfTypes = require('./UrdfTypes'); + +/** + * A Mesh element in a URDF. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + */ +function UrdfMesh(options) { + this.scale = null; + + this.type = UrdfTypes.URDF_MESH; + this.filename = options.xml.getAttribute('filename'); + + // Check for a scale + var scale = options.xml.getAttribute('scale'); + if (scale) { + // Get the XYZ + var xyz = scale.split(' '); + this.scale = new Vector3({ + x : parseFloat(xyz[0]), + y : parseFloat(xyz[1]), + z : parseFloat(xyz[2]) + }); + } +} + +module.exports = UrdfMesh; +},{"../math/Vector3":24,"./UrdfTypes":38}],36:[function(require,module,exports){ +/** + * @fileOverview + * @author Benjamin Pitzer - ben.pitzer@gmail.com + * @author Russell Toris - rctoris@wpi.edu + */ + +var UrdfMaterial = require('./UrdfMaterial'); +var UrdfLink = require('./UrdfLink'); +var UrdfJoint = require('./UrdfJoint'); +var DOMParser = require('xmldom').DOMParser; + +// See https://developer.mozilla.org/docs/XPathResult#Constants +var XPATH_FIRST_ORDERED_NODE_TYPE = 9; + +/** + * A URDF Model can be used to parse a given URDF into the appropriate elements. + * + * @constructor + * @param options - object with following keys: + * * xml - the XML element to parse + * * string - the XML element to parse as a string + */ +function UrdfModel(options) { + options = options || {}; + var xmlDoc = options.xml; + var string = options.string; + this.materials = {}; + this.links = {}; + this.joints = {}; + + // Check if we are using a string or an XML element + if (string) { + // Parse the string + var parser = new DOMParser(); + xmlDoc = parser.parseFromString(string, 'text/xml'); + } + + // Initialize the model with the given XML node. + // Get the robot tag + var robotXml = xmlDoc.documentElement; + + // Get the robot name + this.name = robotXml.getAttribute('name'); + + // Parse all the visual elements we need + for (var nodes = robotXml.childNodes, i = 0; i < nodes.length; i++) { + var node = nodes[i]; + if (node.tagName === 'material') { + var material = new UrdfMaterial({ + xml : node + }); + // Make sure this is unique + if (this.materials[material.name] !== void 0) { + if( this.materials[material.name].isLink() ) { + this.materials[material.name].assign( material ); + } else { + console.warn('Material ' + material.name + 'is not unique.'); + } + } else { + this.materials[material.name] = material; + } + } else if (node.tagName === 'link') { + var link = new UrdfLink({ + xml : node + }); + // Make sure this is unique + if (this.links[link.name] !== void 0) { + console.warn('Link ' + link.name + ' is not unique.'); + } else { + // Check for a material + for( var j=0; j 0) { + var geom = geoms[0]; + var shape = null; + // Check for the shape + for (var i = 0; i < geom.childNodes.length; i++) { + var node = geom.childNodes[i]; + if (node.nodeType === 1) { + shape = node; + break; + } + } + // Check the type + var type = shape.nodeName; + if (type === 'sphere') { + this.geometry = new UrdfSphere({ + xml : shape + }); + } else if (type === 'box') { + this.geometry = new UrdfBox({ + xml : shape + }); + } else if (type === 'cylinder') { + this.geometry = new UrdfCylinder({ + xml : shape + }); + } else if (type === 'mesh') { + this.geometry = new UrdfMesh({ + xml : shape + }); + } else { + console.warn('Unknown geometry type ' + type); + } + } + + // Material + var materials = xml.getElementsByTagName('material'); + if (materials.length > 0) { + this.material = new UrdfMaterial({ + xml : materials[0] + }); + } +} + +module.exports = UrdfVisual; +},{"../math/Pose":21,"../math/Quaternion":22,"../math/Vector3":24,"./UrdfBox":29,"./UrdfCylinder":31,"./UrdfMaterial":34,"./UrdfMesh":35,"./UrdfSphere":37}],40:[function(require,module,exports){ +module.exports = require('object-assign')({ + UrdfBox: require('./UrdfBox'), + UrdfColor: require('./UrdfColor'), + UrdfCylinder: require('./UrdfCylinder'), + UrdfLink: require('./UrdfLink'), + UrdfMaterial: require('./UrdfMaterial'), + UrdfMesh: require('./UrdfMesh'), + UrdfModel: require('./UrdfModel'), + UrdfSphere: require('./UrdfSphere'), + UrdfVisual: require('./UrdfVisual') +}, require('./UrdfTypes')); + +},{"./UrdfBox":29,"./UrdfColor":30,"./UrdfCylinder":31,"./UrdfLink":33,"./UrdfMaterial":34,"./UrdfMesh":35,"./UrdfModel":36,"./UrdfSphere":37,"./UrdfTypes":38,"./UrdfVisual":39,"object-assign":3}],41:[function(require,module,exports){ +'use strict'; + +var UPPER32 = Math.pow(2, 32); + +var warnedPrecision = false; +function warnPrecision() { + if (!warnedPrecision) { + warnedPrecision = true; + console.warn('CBOR 64-bit integer array values may lose precision. No further warnings.'); + } +} + +/** + * Unpacks 64-bit unsigned integer from byte array. + * @param {Uint8Array} bytes +*/ +function decodeUint64LE(bytes) { + warnPrecision(); + + var byteLen = bytes.byteLength; + var offset = bytes.byteOffset; + var arrLen = byteLen / 8; + + var buffer = bytes.buffer.slice(offset, offset + byteLen); + var uint32View = new Uint32Array(buffer); + + var arr = new Array(arrLen); + for (var i = 0; i < arrLen; i++) { + var si = i * 2; + var lo = uint32View[si]; + var hi = uint32View[si+1]; + arr[i] = lo + UPPER32 * hi; + } + + return arr; +} + +/** + * Unpacks 64-bit signed integer from byte array. + * @param {Uint8Array} bytes +*/ +function decodeInt64LE(bytes) { + warnPrecision(); + + var byteLen = bytes.byteLength; + var offset = bytes.byteOffset; + var arrLen = byteLen / 8; + + var buffer = bytes.buffer.slice(offset, offset + byteLen); + var uint32View = new Uint32Array(buffer); + var int32View = new Int32Array(buffer); + + var arr = new Array(arrLen); + for (var i = 0; i < arrLen; i++) { + var si = i * 2; + var lo = uint32View[si]; + var hi = int32View[si+1]; + arr[i] = lo + UPPER32 * hi; + } + + return arr; +} + +/** + * Unpacks typed array from byte array. + * @param {Uint8Array} bytes + * @param {type} ArrayType - desired output array type +*/ +function decodeNativeArray(bytes, ArrayType) { + var byteLen = bytes.byteLength; + var offset = bytes.byteOffset; + var buffer = bytes.buffer.slice(offset, offset + byteLen); + return new ArrayType(buffer); +} + +/** + * Support a subset of draft CBOR typed array tags: + * + * Only support little-endian tags for now. + */ +var nativeArrayTypes = { + 64: Uint8Array, + 69: Uint16Array, + 70: Uint32Array, + 72: Int8Array, + 77: Int16Array, + 78: Int32Array, + 85: Float32Array, + 86: Float64Array +}; + +/** + * We can also decode 64-bit integer arrays, since ROS has these types. + */ +var conversionArrayTypes = { + 71: decodeUint64LE, + 79: decodeInt64LE +}; + +/** + * Handles CBOR typed array tags during decoding. + * @param {Uint8Array} data + * @param {Number} tag + */ +function cborTypedArrayTagger(data, tag) { + if (tag in nativeArrayTypes) { + var arrayType = nativeArrayTypes[tag]; + return decodeNativeArray(data, arrayType); + } + if (tag in conversionArrayTypes) { + return conversionArrayTypes[tag](data); + } + return data; +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = cborTypedArrayTagger; +} + +},{}],42:[function(require,module,exports){ +module.exports = window.WebSocket; + +},{}],43:[function(require,module,exports){ +/* global document */ +module.exports = function Canvas() { + return document.createElement('canvas'); +}; +},{}],44:[function(require,module,exports){ +/** + * @fileOverview + * @author Graeme Yeates - github.com/megawac + */ + +'use strict'; + +var Canvas = require('canvas'); +var Image = Canvas.Image || window.Image; + +/** + * If a message was compressed as a PNG image (a compression hack since + * gzipping over WebSockets * is not supported yet), this function places the + * "image" in a canvas element then decodes the * "image" as a Base64 string. + * + * @private + * @param data - object containing the PNG data. + * @param callback - function with params: + * * data - the uncompressed data + */ +function decompressPng(data, callback) { + // Uncompresses the data before sending it through (use image/canvas to do so). + var image = new Image(); + // When the image loads, extracts the raw data (JSON message). + image.onload = function() { + // Creates a local canvas to draw on. + var canvas = new Canvas(); + var context = canvas.getContext('2d'); + + // Sets width and height. + canvas.width = image.width; + canvas.height = image.height; + + // Prevents anti-aliasing and loosing data + context.imageSmoothingEnabled = false; + context.webkitImageSmoothingEnabled = false; + context.mozImageSmoothingEnabled = false; + + // Puts the data into the image. + context.drawImage(image, 0, 0); + // Grabs the raw, uncompressed data. + var imageData = context.getImageData(0, 0, image.width, image.height).data; + + // Constructs the JSON. + var jsonData = ''; + for (var i = 0; i < imageData.length; i += 4) { + // RGB + jsonData += String.fromCharCode(imageData[i], imageData[i + 1], imageData[i + 2]); + } + callback(JSON.parse(jsonData)); + }; + // Sends the image data to load. + image.src = 'data:image/png;base64,' + data; +} + +module.exports = decompressPng; + +},{"canvas":43}],45:[function(require,module,exports){ +exports.DOMImplementation = window.DOMImplementation; +exports.XMLSerializer = window.XMLSerializer; +exports.DOMParser = window.DOMParser; + +},{}]},{},[6]); diff --git a/roslaunch_editor/www/switch.css b/roslaunch_editor/www/switch.css new file mode 100644 index 000000000..d0ec8c3fd --- /dev/null +++ b/roslaunch_editor/www/switch.css @@ -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); } From cda7e62dbfc271dcf415ea4a0dd5f5d773e8a484 Mon Sep 17 00:00:00 2001 From: Oleg Kalachev Date: Tue, 21 Jul 2020 10:03:40 +0300 Subject: [PATCH 2/4] Adjust launch file arguments for more convenient configuration --- clover/launch/aruco.launch | 2 +- clover/launch/clover.launch | 32 +++++++++++++++++++------------- clover/launch/led.launch | 11 +++++++---- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/clover/launch/aruco.launch b/clover/launch/aruco.launch index c4ce57811..326e44a87 100644 --- a/clover/launch/aruco.launch +++ b/clover/launch/aruco.launch @@ -1,5 +1,5 @@ - + diff --git a/clover/launch/clover.launch b/clover/launch/clover.launch index 41b704ca1..49f9ad27f 100644 --- a/clover/launch/clover.launch +++ b/clover/launch/clover.launch @@ -1,16 +1,15 @@ - - - - - - - - - - - - + + + + + + + + + + + @@ -31,7 +30,7 @@ - + @@ -87,4 +86,11 @@ + + + + + + + diff --git a/clover/launch/led.launch b/clover/launch/led.launch index 0c74f4957..30a5a5ab0 100644 --- a/clover/launch/led.launch +++ b/clover/launch/led.launch @@ -1,7 +1,7 @@ - - - + + + @@ -22,7 +22,7 @@ - + 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 } + + low_battery: { threshold: 3.7, effect: blink_fast, r: 255, g: 0, b: 0 } + From 4fe6f24d166207dfbdb462072bc53aace906cd38 Mon Sep 17 00:00:00 2001 From: Oleg Kalachev Date: Tue, 21 Jul 2020 10:09:09 +0300 Subject: [PATCH 3/4] Edit readme --- roslaunch_editor/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roslaunch_editor/README.md b/roslaunch_editor/README.md index 9a9e49ca7..592c5c0db 100644 --- a/roslaunch_editor/README.md +++ b/roslaunch_editor/README.md @@ -2,7 +2,7 @@ Web-based ROS launch-files editor, created for making configuration of your robot more user-friendly for novices. - + ## Quick launch From c254691f8049934c19ca6d7ddeb8b333c4a7133f Mon Sep 17 00:00:00 2001 From: Oleg Kalachev Date: Wed, 5 Aug 2020 02:06:41 +0300 Subject: [PATCH 4/4] Adjust aruco.launch arguments --- clover/launch/aruco.launch | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/clover/launch/aruco.launch b/clover/launch/aruco.launch index 326e44a87..e5a9b1f66 100644 --- a/clover/launch/aruco.launch +++ b/clover/launch/aruco.launch @@ -1,7 +1,10 @@ - - - + + + + + + @@ -12,8 +15,9 @@ - - + + + @@ -24,8 +28,9 @@ - - + + +