From 38fd577a1efa5248a91105c1a907cb0f364a6dfa Mon Sep 17 00:00:00 2001 From: Erik Andresen Date: Thu, 2 Apr 2020 07:19:40 +0200 Subject: [PATCH] www: speed control --- www/assets/javascripts/application.js | 8 +- www/assets/javascripts/roslib.js | 5943 +++++++++-------- .../images/ui-icons_444444_256x240.png | Bin 0 -> 7006 bytes .../images/ui-icons_555555_256x240.png | Bin 0 -> 7074 bytes .../images/ui-icons_777620_256x240.png | Bin 0 -> 4676 bytes .../images/ui-icons_777777_256x240.png | Bin 0 -> 7013 bytes .../images/ui-icons_cc0000_256x240.png | Bin 0 -> 4632 bytes .../images/ui-icons_ffffff_256x240.png | Bin 0 -> 6313 bytes www/index.html | 10 +- 9 files changed, 2986 insertions(+), 2975 deletions(-) create mode 100644 www/assets/stylesheets/images/ui-icons_444444_256x240.png create mode 100644 www/assets/stylesheets/images/ui-icons_555555_256x240.png create mode 100644 www/assets/stylesheets/images/ui-icons_777620_256x240.png create mode 100644 www/assets/stylesheets/images/ui-icons_777777_256x240.png create mode 100644 www/assets/stylesheets/images/ui-icons_cc0000_256x240.png create mode 100644 www/assets/stylesheets/images/ui-icons_ffffff_256x240.png diff --git a/www/assets/javascripts/application.js b/www/assets/javascripts/application.js index a7b4785..2fc4176 100644 --- a/www/assets/javascripts/application.js +++ b/www/assets/javascripts/application.js @@ -189,12 +189,12 @@ function init() { Y = e.pageY; } // relative click position - var Xrel = X - this.offsetLeft - $(this).width()/2; - var Yrel = Y - this.offsetTop - $(this).height()/2; + var Xrel = X - this.getBoundingClientRect().left - $(this).width()/2; + var Yrel = Y - this.getBoundingClientRect().top - $(this).height()/2; // scale to -1..+1 var trans = -Yrel / ($(this).height()/2); var rot = -Xrel / ($(this).width()/2); - setSpeed(trans, rot*3); + setSpeed(trans*$("#scale_trans").val(), rot*$("#scale_rot").val()); } }); @@ -238,6 +238,8 @@ function init() { // reload $("img").attr("src", $("img").attr("src")) }); + + $("input[type='number']").spinner(); } Vue.component('input-value', { diff --git a/www/assets/javascripts/roslib.js b/www/assets/javascripts/roslib.js index 2193da8..fccc080 100644 --- a/www/assets/javascripts/roslib.js +++ b/www/assets/javascripts/roslib.js @@ -1,4 +1,4 @@ -(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 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":10,"../core/Topic":17,"eventemitter2":1}],9:[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":12,"../mixin":24,"./ActionClient":5,"./ActionListener":6,"./Goal":7,"./SimpleActionServer":8}],10:[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":2}],11:[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":13,"./ServiceRequest":14}],12:[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 { - this.socket = assign(new WebSocket(url), 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":13,"./ServiceRequest":14,"./SocketAdapter.js":16,"eventemitter2":1,"object-assign":2,"ws":39}],13:[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. - * - * @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, - args : request - }; - this.ros.callOnConnection(call); -}; - -/** - * 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 - */ -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":14,"./ServiceResponse":15,"eventemitter2":1}],14:[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":2}],15:[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":2}],16:[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 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 { - var message = JSON.parse(typeof data === 'string' ? data : data.data); - handlePng(message, handleMessage); - } - } - }; -} - -module.exports = SocketAdapter; - -},{"../util/decompressPng":41,"ws":39}],17:[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' - * * 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 !== 'none') { - this.emit('warning', this.compression + - ' compression is not supported. No compression will be used.'); - } - - // 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":10,"eventemitter2":1}],18:[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":24,"./Message":10,"./Param":11,"./Ros":12,"./Service":13,"./ServiceRequest":14,"./ServiceResponse":15,"./Topic":17}],19:[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); -}; - -module.exports = Pose; -},{"./Quaternion":20,"./Vector3":22}],20:[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; - -},{}],21:[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":20,"./Vector3":22}],22:[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; -},{}],23:[function(require,module,exports){ -module.exports = { - Pose: require('./Pose'), - Quaternion: require('./Quaternion'), - Transform: require('./Transform'), - Vector3: require('./Vector3') -}; - -},{"./Pose":19,"./Quaternion":20,"./Transform":21,"./Vector3":22}],24:[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); - }; - }); -}; - -},{}],25:[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":5,"../actionlib/Goal":7,"../core/Service.js":13,"../core/ServiceRequest.js":14,"../math/Transform":21}],26:[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":12,"../mixin":24,"./TFClient":25}],27:[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":22,"./UrdfTypes":36}],28:[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; -},{}],29:[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":36}],30:[function(require,module,exports){ -/** - * @fileOverview - * @author David V. Lu!! davidvlu@gmail.com - */ - -/** - * 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') ); - } -} - -module.exports = UrdfJoint; - -},{}],31:[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":28,"object-assign":2}],33:[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":22,"./UrdfTypes":36}],34:[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":19,"../math/Quaternion":20,"../math/Vector3":22,"./UrdfBox":27,"./UrdfCylinder":29,"./UrdfMaterial":32,"./UrdfMesh":33,"./UrdfSphere":35}],38:[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":27,"./UrdfColor":28,"./UrdfCylinder":29,"./UrdfLink":31,"./UrdfMaterial":32,"./UrdfMesh":33,"./UrdfModel":34,"./UrdfSphere":35,"./UrdfTypes":36,"./UrdfVisual":37,"object-assign":2}],39:[function(require,module,exports){ -(function (global){ -module.exports = global.WebSocket; -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{}],40:[function(require,module,exports){ -/* global document */ -module.exports = function Canvas() { - return document.createElement('canvas'); -}; -},{}],41:[function(require,module,exports){ -(function (global){ -/** - * @fileOverview - * @author Graeme Yeates - github.com/megawac - */ - -'use strict'; - -var Canvas = require('canvas'); -var Image = Canvas.Image || global.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; -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{"canvas":40}],42:[function(require,module,exports){ -(function (global){ -exports.DOMImplementation = global.DOMImplementation; -exports.XMLSerializer = global.XMLSerializer; -exports.DOMParser = global.DOMParser; -}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -},{}]},{},[4]); + +},{}],2:[function(require,module,exports){ +/* +object-assign +(c) Sindre Sorhus +@license MIT +*/ + +'use strict'; +/* eslint-disable no-unused-vars */ +var getOwnPropertySymbols = Object.getOwnPropertySymbols; +var hasOwnProperty = Object.prototype.hasOwnProperty; +var propIsEnumerable = Object.prototype.propertyIsEnumerable; + +function toObject(val) { + if (val === null || val === undefined) { + throw new TypeError('Object.assign cannot be called with null or undefined'); + } + + return Object(val); +} + +function shouldUseNative() { + try { + if (!Object.assign) { + return false; + } + + // Detect buggy property enumeration order in older V8 versions. + + // https://bugs.chromium.org/p/v8/issues/detail?id=4118 + var test1 = new String('abc'); // eslint-disable-line no-new-wrappers + test1[5] = 'de'; + if (Object.getOwnPropertyNames(test1)[0] === '5') { + return false; + } + + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 + var test2 = {}; + for (var i = 0; i < 10; i++) { + test2['_' + String.fromCharCode(i)] = i; + } + var order2 = Object.getOwnPropertyNames(test2).map(function (n) { + return test2[n]; + }); + if (order2.join('') !== '0123456789') { + return false; + } + + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 + var test3 = {}; + 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { + test3[letter] = letter; + }); + if (Object.keys(Object.assign({}, test3)).join('') !== + 'abcdefghijklmnopqrst') { + return false; + } + + return true; + } catch (err) { + // We don't expect any of the above to throw, but better to be safe. + return false; + } +} + +module.exports = shouldUseNative() ? Object.assign : function (target, source) { + var from; + var to = toObject(target); + var symbols; + + for (var s = 1; s < arguments.length; s++) { + from = Object(arguments[s]); + + for (var key in from) { + if (hasOwnProperty.call(from, key)) { + to[key] = from[key]; + } + } + + if (getOwnPropertySymbols) { + symbols = getOwnPropertySymbols(from); + for (var i = 0; i < symbols.length; i++) { + if (propIsEnumerable.call(from, symbols[i])) { + to[symbols[i]] = from[symbols[i]]; + } + } + } + } + + return to; +}; + +},{}],3:[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 : '0.20.0' +}; + +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":9,"./core":18,"./math":23,"./tf":26,"./urdf":38,"object-assign":2}],4:[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":3}],5:[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":10,"../core/Topic":17,"eventemitter2":1}],6:[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":10,"../core/Topic":17,"eventemitter2":1}],7:[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":10,"eventemitter2":1}],8:[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":10,"../core/Topic":17,"eventemitter2":1}],9:[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":12,"../mixin":24,"./ActionClient":5,"./ActionListener":6,"./Goal":7,"./SimpleActionServer":8}],10:[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":2}],11:[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":13,"./ServiceRequest":14}],12:[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 { + this.socket = assign(new WebSocket(url), 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":13,"./ServiceRequest":14,"./SocketAdapter.js":16,"eventemitter2":1,"object-assign":2,"ws":39}],13:[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. + * + * @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, + args : request + }; + this.ros.callOnConnection(call); +}; + +/** + * 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 + */ +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":14,"./ServiceResponse":15,"eventemitter2":1}],14:[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":2}],15:[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":2}],16:[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 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 { + var message = JSON.parse(typeof data === 'string' ? data : data.data); + handlePng(message, handleMessage); + } + } + }; +} + +module.exports = SocketAdapter; + +},{"../util/decompressPng":41,"ws":39}],17:[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' + * * 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 !== 'none') { + this.emit('warning', this.compression + + ' compression is not supported. No compression will be used.'); + } + + // 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":10,"eventemitter2":1}],18:[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":24,"./Message":10,"./Param":11,"./Ros":12,"./Service":13,"./ServiceRequest":14,"./ServiceResponse":15,"./Topic":17}],19:[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); +}; + +module.exports = Pose; +},{"./Quaternion":20,"./Vector3":22}],20:[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; + +},{}],21:[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":20,"./Vector3":22}],22:[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; +},{}],23:[function(require,module,exports){ +module.exports = { + Pose: require('./Pose'), + Quaternion: require('./Quaternion'), + Transform: require('./Transform'), + Vector3: require('./Vector3') +}; + +},{"./Pose":19,"./Quaternion":20,"./Transform":21,"./Vector3":22}],24:[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); + }; + }); +}; + +},{}],25:[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":5,"../actionlib/Goal":7,"../core/Service.js":13,"../core/ServiceRequest.js":14,"../math/Transform":21}],26:[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":12,"../mixin":24,"./TFClient":25}],27:[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":22,"./UrdfTypes":36}],28:[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; +},{}],29:[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":36}],30:[function(require,module,exports){ +/** + * @fileOverview + * @author David V. Lu!! davidvlu@gmail.com + */ + +/** + * 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') ); + } +} + +module.exports = UrdfJoint; + +},{}],31:[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":28,"object-assign":2}],33:[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":22,"./UrdfTypes":36}],34:[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":19,"../math/Quaternion":20,"../math/Vector3":22,"./UrdfBox":27,"./UrdfCylinder":29,"./UrdfMaterial":32,"./UrdfMesh":33,"./UrdfSphere":35}],38:[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":27,"./UrdfColor":28,"./UrdfCylinder":29,"./UrdfLink":31,"./UrdfMaterial":32,"./UrdfMesh":33,"./UrdfModel":34,"./UrdfSphere":35,"./UrdfTypes":36,"./UrdfVisual":37,"object-assign":2}],39:[function(require,module,exports){ +(function (global){ +module.exports = global.WebSocket; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],40:[function(require,module,exports){ +/* global document */ +module.exports = function Canvas() { + return document.createElement('canvas'); +}; +},{}],41:[function(require,module,exports){ +(function (global){ +/** + * @fileOverview + * @author Graeme Yeates - github.com/megawac + */ + +'use strict'; + +var Canvas = require('canvas'); +var Image = Canvas.Image || global.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(); + image.crossOrigin = "Anonymous"; + // 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; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"canvas":40}],42:[function(require,module,exports){ +(function (global){ +exports.DOMImplementation = global.DOMImplementation; +exports.XMLSerializer = global.XMLSerializer; +exports.DOMParser = global.DOMParser; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}]},{},[4]); diff --git a/www/assets/stylesheets/images/ui-icons_444444_256x240.png b/www/assets/stylesheets/images/ui-icons_444444_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..c2daae1663d47f397a01fd4166bce9b6b9ac4183 GIT binary patch literal 7006 zcmZvBWl$WJ6)kLh)UaF?Rpqdju<$Xc!K-1EZvg;m2Sr&a zZO@tgOdp7qykC!mO||mGuP|d7Wy>h`8zNQulJl2fredx2hR`Y;;>y}w?PT_8MN5ht z%y8}EASEyDqJV__?4o(>3BwohAk8Kk&8ZRCGiE!+DT9hXg&JS5v?_IepEqpNV=_Bk z_jnCv@Kv}BHMdM&40xHCnz@SWBL4=&3BQtS(^upnf_gBk$Cwk1d)ck=WS(Jomd zVU_Ed+?DU~na)4;FqtZOg}8>W0-80TNdj65D=+VmqC1_TP^g3HS12mpIa7q-S&-{9 zhGO$Sgs#{*Bce>Xu0cx2Nydf!izy!Z)qCY^vpaQh@gD)zAlJ}h!Wmc`s|N|*VDZ@Q z*Fr`Gm3jn%iWj{=4>Z`^JBC&Lwrs{@j}3lPSifie3|;^k<-5UMUj%{hZcbH6@*hgt z5$!dVHt8orKe9>)%=?~q+_O`g{@`hwV6B{t4G!i+#hdx?7rGxDy$ZyKQ9#2w@FY8y zF|wEc1&Xau6M&!AUqVv+AiKY{hqam>%mb08dX#mlau)npM?nyWFPjY}uoi?!AUe9kF?^U83$&t_36-%@wV%69TZzUz zJ%RrU#uz|NzyR2?6qnNcL>ag+wH9L(NyQ4jXwA8%?xH)ZeyIL(M z$s~#X^#qMQ4%6}+1E{nz1nFj8e7i!l^xkjY@tMza3^9$PoZmB_XykUkFsN8yDV4w* z*;DgExM&!ja5>h}Tq)Ic(@1q-}&rp)o{IvN)=*qG`QXT@4OJa|L4^2|Ay zb)>%u)0%yV`AfRBZlF63_?y|gN=NpYiC{>5qY6`LJLigfgLV)vHA>ZO;_EtQ?E69! zt0&R86CCp>Mim`6?BfZbDB4GZ#MX(@73bXLo^0hh8TB7i;UDy_oNQNKyE!3m&<c?C!c~UolHO_?w+1^~7nm zR@CO*(G53MrX5&fn$a6v_c-VFnLS(-DrzW64QST3YJ%6PT(mbyWZ0~#+w^Ua1d(Qs zu$}3|_f(kE1cuou3BPG^lmKxJ@b&FuaSK3219@~7jfCEj zseTOCss41MRf*z+7E(htX%!%;YHj#0&mF%_F4QF_I)j!LPWJWQ!f|aHc{(fY3U-&7 z9t|yF{b>C^1;!KdA#@?CXr>3)O|4v#ogru4`3b=iluYe!!jEjJ|0%$K&)Etd1s8XW zwF|T<(<;4sZNJ>Y=ElV$lLl1Pl$fO(*8vuHnlx(GZ6;YQ5@H*+0Ctgdra^0zh%LZ@U2VxYn8H_XqANq1|qXIE6t0I|< z_PoX@MNwIxVHj=$rLvA$O`0oz%}kSho$9IU@vbK&&*4tB$;(yo_&usIkcNNa8gV&A zw{9wFNn?@I)4+xT8s5ufNvfOp(6gogb>=-n9=l8eoj|@$1fzKYovN+As(^DybSQG= zJk*W5F!!;{Y&C{HG<{{WZhGt0@Iv|zZ&>M0m3RmCJir*M3INwD9xR<#tZ@9T6DbMj z{8BP}AXgF~ivN4OQ@usUrNmLZZBFtLv_>B;rfS%(Q6UyO&yB|yuQVN>{A&+N!Hi5* zXT43XL)PAJD;wgupA{fLgQPSuCk;=1S)}=(M4EXY?=qY!)qskBRdz@!t z#DPfeIE5!|b``hIUf%1vEkM>2@_2ztM|H%$(n}IQ(E|6t>})I$qsO?U<%0T)#<(K| z%?%DzQ*qzM+Q3Gm1xrCg(_75gVdA>RI9tj&u4p9w)8;@g0u3yD(VN2{GZ&)ev>!c` z;)c*3MEn1^UZN8LDh03b-}xy%Fu+y=I>2t_feHZY)i6V=8fPWK_pk53AsGqocwekm zNO!j{+^qJB->-a*7pzBoaIZ%QCNptcaoioly^WG!2*)TK`o259&3I=v5jr|&u2d-b zE9s@ZWC++#yvIxR&vbnQ^N=lhlH8iiz)3DjT7M~HtZujdXcY1#yPsJ*`9O$ExmEor z^7Mv#vicV#Qu*uQv4sO((FxLvB((#5h?GZm;%|*eq4H+L3los3YBg?cE>XOI^@y3` zJ80$UtYcu!fW=m&+L}I7J2~U77QwEHG81=Z-t2LeMnh4^)#cad&*<<-3@JS|xUR_(^4ND)g|@-l$NFHURNEOvp0485Q-KddnpdFH%@h@3Eqo zvUl#TPU_{tXjjfoZw!;IgxUqrjjUML_c7BA(9#Ew1DTSYK?vkS?G@JXXK;!e2aa#t zyfkG!b^;)pMr-tp2Kf=-DNYs`FO1L&QIO5(=)l8%+ut9(_>9<*98hNG=<6zhlqrpTH(I_Zzr+pELxl#TFx@foO4^CoCfd&7E1 z?By%$-E~T(FDllQ%F(sOAk*#s@FlMe_^Q{S`w&}AnpMhvDU+mA(lKNU+~-J5ka=7+dl^$#L$e7TWaya5D}j?SMQE-OmHmn~h=dV0`X2 zaHn5@;qTjb#2=VJt?-)xLCrDc8CgCi1NEk0RiXtv@JAYV(s9kBMgf_k3)d#%Nv`y) zf;~YTs8A9IdnVJ(Lk2~Gi5y%f*NcF=%yaOS43?+-0pFtR@7ooI-B3LgB=NQQFZEGK zlBlQjUMn?SQ92L3hn6V(q2_3q4*2)(_k` z&>l%{r5ZpS?*Dvn83W$pWo?YEGxdT-oQ2?Ju>QfXj&QB_W8W|g-H$l}QZ%jwzvgjm zY?a8d{C@A>MCb}_MgCBSsja&=hN=HmEUyenw?SO3!7iz%a{Kv_gTV1^! z9o6(>5t!Y&WLy;{< zvqOHreG2b_#vYkc{Ar67vzw8f!2A&2nPb#0Edt>Cgfb4+L;hx(b}9NtddtucGea1` zo4t{O{@8P;Nr{t2-z2Y=ZY?tztQUJGW+hzxsKa#}wPQE6W2^PWTeTCj9Q#_Akfcdepbe=+hfM-x8t?R{3oJVdVg5lZi9J1xmCc3{?u8 zg+ZZ^5WM8muTv}JErULn;6pb3GLrLBpVqqA^7MDI1lA4hVP8b=N`1q7uDIh)%n)lo zx@=I53EIx(UxcCsg`Cj-la))IY^j^5^eFhqw^Ym$#dhU8z;5A~> zD)xjnMyJ+aBAZ||lt+ymz<0AAIa4FM$C`DIqvt|6=}f41Y?F~;4Uqcmu*kTj2 zco0@in_OXJC^kT}z4=3=X&HbA-Gmy3kNa>w*zTDm<*f@qr=m_`9N&`Wk_iS}-EnJn zo$aYFfQXA0K;7>s1iD1vED^tY@434FjOSp5V4jyM6*=dVML>7(m;8*@o{M6>{;QX< za`<9Lw0}-Hb=`MP1ufi=xRl#Tu>_UK8@;bY{_fCZb3O>WsnPU7qOITcS78;E=Xb@fyck%jd;;Mee#steS2ck^npioDQ3sT$ z{+8Ha*{kYT_k5=q5P5>~v>P>f^&yzVb@UZaq&LGfR>5e_o7bH+x<8u*qTi)~%E zgu44f=jb<90pEWE3owb*MGx2opyChlm7YE0aNbY_G*?FOuV&pkBbMKQcPOd?zQcGh z7BmaE{Tn<@OW2ix(;UPV9GrmW0A-Xm>L%(TL?6+h^ z$JY6PPNA?tu%mM)(?{m1XO!uGPjPe-Nkorj8Y-+SR5$`;oUR-*hoyt=X)dX-E-{Rk z0=FuQfbOsrJ=yv2HAjR7@)gaf2Ux!Tz8u3*McPtEa_xc1ThWfN zVE+@NKVDsDVJ?vAT%sqlDatUsMlL*js?}QIfwXON)A;R zEI~3tEQF1P0z@h3%ZKl?`a^9CM3}BJVRfo+o2}k1Q{{XYaEh-8wEnSG82Mn}_{(50 zZ{P8}`K@g~y(*7~P`8?Brf9qi4w}a?v4pANtP-P?w=sjnpy)tFCEcfAUpGu#43%xj zd|0hg{jP}QVE#%E7z?zoSN25#bKkbRPNJ)6qjaJk);DpPUP?0qas@m5_k79$k6pX` z2?R(-{9i3mY!hI>?k;q3sD3Qbb!dxt+=5;TfnLsym%-H(+)jToJ(uKjIhT!hs7arT zDMWU0Z=kj=J&&l8FwD96u7&rVgnI5p3$=ZG>`$X}BA+<7w)B?C7XKSeO+X6tb2l5^ zu25NYskaCQrhJ+F>adP-u}CC74L7~ueG?eL5fVd(tEWn8AX;|SQjvfU{C0^s{Tn%3 z$0`v0(tSC?j~Q7YRLAyQ_$3?H+>o*{?=aiSwDdsr02^Xim(xzm$z{v2bh$;k5SVMc z7&!?~x`0^6NRA^%J zoFD?=x^mn~GyC28Rc!kM-NW_>2AC{qKz4g~>M-(T&z9?FppA1DmC-q`8juEp1o}0k z*Z~y60KcDT@kjKQRKzkx4;>w>A*>;j)r$v_!sz5e!>{{`$S&Csm`is?V}-~BUO{^4 zrbs&rZEIix=2m{IVNe>6&vg-R`Oe24-X211QXhlF8<3y^y;P7pri!` z%HqXx*?*#~dh;xfH(G~jC8rp0C5wKvKM&m+PbLa(r-~xki~U?7-P4d{wkS0Lc&1w> zo&ENuD2D^?GhGX18Q6Fb_OF%NqHHcDwqjZ2(nGFBkQ_PwnTt(pe|e!_U{l2sw!LV! zQR27{2|isXifE5(XtG5v?iBtw%pr#gSV!v#?;^XO@V%gB*kuen8iO1t zYK^p!2Ylit_LGp-1Wd4lorx>D*!j@wNhh%3X2~gxezh5;DeC7^*@Z)ttC3^K_81%) z94~@wnwx5(YZ{C&`VCESX|Nuf`h4oq1WHuoJGDNf@IdAtChS(p^#9O&UO?{5<}*qu zOVCK~rk~@s z>8M}eN!b`IRr^NA-*`bawh4r#qsT;bZK~w-x~>+h@;d!T#rVU~v*l7%9RUZcR1SH>;IOa|*|Hdqij0P2C~^@9jg`?ejn1<&>u zo+|W@1hAYjnyiLB^u-!AUxt;`E`dQA&|l~JPD?8EBK06gDj(>spV#5*YwL{w+o#j8 zfj0D)S=Fi3rb>~W%JqvhK1vrO3~A_ZFU%m$fp2W{T6h>{@ww4(tBa;?!DBS{Gf}3kG!(sztgF_K;%4@MymfS!f&Urdbi>2yYj{CZL)+WjHv^( zBIVlpv|YjG#WNcIW?m*>U1qD!h&XC;^^@B+SU%~l@S@Q_xNuUEAKeo1&}H%N8`+}l z+P23xz5a#FyhXq)#t-iPT+otK9EfJh4(u2u~y^WDN zZ-Q&;69;KK7chLKy#F+weps0u)vNDtkD<>NHziNGe1=<0x*dRyGO?fK^72OG8cC9| z%LVkFpP2(8r0olp|azgqOgZ$ZV zP3UrTp2HdG)8`T@;@jfhj957!r0^p5?%50{mF-X40~ zVX9UX3lsd<(|RAKj1a2123+vIoc zyP+&{jOj#w^djH`{u0=yfR22i=nWP?4_|}+9FMlIGC-PSD38=g0A-BI|De5h;R2=0 zo}|t;X>=zk$a**}!Z85UthO%8WM7>YMcmor9c|&<)3guD&OiIv_p|qUVqI%pYpr{|(bG{SC1M}~0Dx3oP1yhdK)0{J0Risq zQzh3&;da1vex&^f04fuSv37X380Mg6pbY>aTmXQG0)UI#Da0lK_zMBRwk-firvm_; zS9YVm?Cl4zy{4)%U{o3ZTKyJ5{M5BoAd3)EJO+uC*LNNR0K+SF3hiT*iM~4%6w9Gks)JkIiqk6Pz8>;c|pm=;?ZcsaaNHY#MdyI&y?Mz*Pl_9UMi5i+I-KPDKj{So6Pg<^h) zvho&g>N2-(#@=;ZXtX$bFSF`vyaj<7x$PccWfy|K+okFkXh@xD`1#B|^EFI(E#zt_ ziIt~?$^{;)HDYW0*$BxLO0@1gksavEl3wWk*SQWlK;0hiwP-ZqR4CFhq@R&UB!%JkB|obx;QxmZ{T{<%t{tzuPZl_9W>3XaAMVqG8D`sa|nBAwFA-`L0Zfo z9}|B4h5vM~hEq?4-qE0*tVs^>#45mrf1LW? zE|D#|NkYQZ2`W4KZNtZ(bK|?MvaiWWA_B=5hfEqhI!(vnX^_Q@vi*6|bPgflQyciW zE$5C&%zP+RWY4WSIrW^=i_nN|&&&X@IK+ttZ>~VdXjToPM)P*OzuVX*eMheCy;Iu} zw-w0GP{`+$m}0i)y;-JMG=Gs(`?e)Hg4ypNU!U6buFBt4n3e-X<8x5pq*ZdCCNg>y zCRv=5`U86>rhz1HheOCOD8xPAa>{I%KG1Epoz>qEr=h^27GEndPxH^IG*ACvGrp5m zwkA*Q&@A2`dUu_!Up?rWDOF@H_Kcgb`Sue<)o>Rlelf*AfUOp=V|`e6Wp zG|3;pI}7nG#Xe3hPoF++nlFTkfI04dvYc%(DMifZo152VH%%hGPr4T+xTU>i+Tu+K zyAY}2`>l{Z$eB0K_-wv&>*LsSzOs|I;KrI+OQlWpzA;W?XpQfT+f1RoGiSw#!bx2b zoy6vVMVIC5=b1X&zY;NxX5&-7d76EDopZUqlF`S{5*}5$MrO~}Rm!?CPF{EGX-t0+ z`o;St(muTk*Q*?nr)>F+F}-wj?fSIk>f&<7eulud9LE>u51}nr(raLC9$ZCK^0A5_ zO6=$7=<(vMYonyKrftSfzq6N>V)Px8%(z--y7s6Ipzgis0%_6JFvjvj4MfNYAsa4C zvyd%iNU7}_qf(vq@^fapj)Ebfu`d~iv1aiiz9G7-sBgZ0ExAewktJp1;!kPEjQ6)Y zpw!x5YPNUrg3F`G55@A#0g1!2bcGY+Vxg79L=Dl|5=>pNwOQk@$%THjp?_rEM{ z3HLwL{!C6Vt(cU~-}G%^+5S_#c-5b+g)-t{+wrMsd&nQJWuu(BiJD+BzvRX=Kf}CS zoXvQ$p$n9|ONo6Ag`8ACHE#=fB!!8Ujovb9yA^dg6DUE>DFW_~?L30?t3tkT51TxM zD-;cu)`d$G8~(;qsl9A`RkE`3iM*cxc8{pkYbqcaVKr?Y=tmO8tURaGk3>@j!7578=k7}%9vxKoUW(y1%ra~9gi zpzy_ChMV6l$?8SKRXvd;+<8?sIsB}0TY2Is)f-lb#eoogIoStcF)OrFM>+0*izzx3 zEXMqAe*ce_yHpXsD()vs-~@x~x2m-7WsRz%h{rxQ@s`_1o<<6Z@oWgpYtxA8v&&mi zCD+6+p&Gx82q|(SeurxT=zF!nwKBmd3%^t-!Nes9&&h05T#IgS z!Wsb^J7QbT5p%otF62Wkis62MUax{ky zP9j(2S@%+D3{>cCmNhKYu99|)ms0QtLR*v<7hpZX5f_Tuh zNk)}FxJCjvQq&*5(`ZuA+k}9Z_E6#d?z$XJi3e(|NIF{?uWrWxvonskS}~8b-~H`} z<`G8NFE(%NLm-AiE`*Nu!)){jv7=G~1Z|sdT;Xk7(0n z^?X9R3`xyY86>fMUk8ov~#sbS-|T18b@ z+R7xUH(=i(H@Q2T60HfEZD=V*haB$!Kyafg-i^SE2tj8wYX?of?+W=rB^;Fj1jj;> z_;ES47lt7*ha6rJ$OBiU)4pZhtL0@0rJbj_?+KFn?DF+kqYGP!7GVX@^cCAO)&6^W zHt%MF?q9(ZM95|8V-Op6eK2YG84VDz(iI$)duu!&#BLT29v%ABQBBqUvnsT+-z>=P z%OEk$sVpUYF6o-XMs)thTjc499kU<>?2|$zI&pO1C9Q8@j&QLkmO2o;Z}*t~M(m*G zpe06XeWGnxWgL5UD4PK;)9gU4kF4MU**(60DChteE}lAOSU$kG?(LBd)TA_*`XYnf zSEvuaed8-g8wJrK=%sgfNkGBc&n7*J0t5-`D3GfjuiW&}ENj8-O~CS_wkUzTDJ%wT zZL7UvGVZy8m+=*X7+iiGmOU7x!HQhtB?BdjmTut;og2-kzz07K3otv%i?&Zk4IR9Q zeDs|WlA2JXc|KuBa@U=89tCTMMu)dBCT!@(X{knR59G16Aw3g(ov~77Nz);Sk}-Co z66-D}&IT0S)Vs?kMwCMoavMh!)p2+f^Tf1bT?$bc1}kjHFvYX1x1q2ifMD2@tv&mo zWoiqWb%Vwx9G4RKrn&5a)}iIWPgj#j^rxL$WA{Lc@Hd!FK3nBC*i@Y|zdCj1Ss0e4 zGP`xH-KU2lgaewC8J4~Satco)YD0ecnebbMAa;HK={6<)r_=cny9P}38-N+7gJ%mY zd$0XJBI{VEL&U=_LJ^QT+1=_BFR9=O(rS%_i|o8Xfz~w^w)fX-{CD53uBt-MXlmCk zFJX}Lh(fq@!^ta=AS>)hJm&{f4R}m>IDg|12u(DDC7r)ht{^wS-ki=`D1GL73RVpk zHd%XLyLi2qwRyUB;5$oYlVH$fmcK8PdHu&CW*?Q9Xnan4!KT}Z+n!vohrj8CAZ%>M zox@&A^EbBZmu(P)K57VChU@^jO9o220zGl+gD-L~mBGrxEUpu=P=e{S88gGv;{zA@ zrQ#rJ;H-Z97s+QfNJEP|h(({$n+Qo>?rEHVJ zUWvM|1g0JpT%?76PZm#CODx(B(1#b~Mad7G#9p%4$3ERF&YH4_*@ zk)z8O;lFv~2c0rDgbZK0!h?iw20YO$SlWeE52Dn8rtoglXXcdh?{M%j61`%t1vL%2 zHhv$;Fse-?*7XbA+yB@rjwLRqgomlyjUSQOa#uh)=I^n#Pjd7^$2K}z#X4>;akD;{ zy*%g+El@{~(C;%b94}FS4rMSRKW4HX_=eGL9UiPw*Nc@1Oy0aKrWrtL!6n*+`m3c7 z@$Neb2R+IDlJEEoTcd-%KH)2$oR>dC#HO^bZW+xKBvd2s2CKUN(06p^>EVA{p;A4Z z2a^#~Rq!t?=CjRfn+`RFjdJsn9G1p{8>alPiyguq&D&O zvbVA@F^RU93_1D=C-`1>_g6q=khfk;-_RiS#%>PsLs2fYH(KXoRUoG7frgq|nt+`< z{TIQH0{(^@@*~5Yb514)|YtT~g_uk=T!Bq|O3%3~9e z_Y>c>;OkVkt25)z_cna~Q=J~O``%+SZSs;TWoXnc6R+o^hTjHtym$6orX>e)m86$o zPim)0;ljuPQ;+#3Ve0@?)Nk2ubWY#ezZxVe4Mrco4NN~8q@yZ;j((h9&;x>iPxuTJJ1z!5t&-1w#(6%Ana5P4~ z5L-Ii4;57NzgWBcUb=EJNHK}pxR2f{Q=DY4>5Yt_QK^iM;|VIfjS=!|bO09C03!pD ztS#%V1NJlQ2{1S8bezyxxN&J;RWQ^NQt(DiwMF4 zX0bg_=e(DY4Cu^#{p*eJ;NCmVmah7v_T*kMYLymI({q6eY4iaVaheTc)%3|{_>YI> zi;kht^;RRp)ATAAC2shIo!PW}0-Qg7QG*Dft?i@ zYS9i@((#j|OCn!D-|R&Md1@}sEN&%DuYqyXG=)%CgY2M*R5!j1jg|Yli`A}=RWpi+ za`if+5G8ZJJ0a~Q-8-=`F^Zl3$5JrSAPdI`1q?PI2r+lqW{>Uya|aOwy>}_n?`A-* z5oh-`X!w4Sii=Dju1AD6AZ+4pBptpjn~Ol|sZMZeu%j?a{|Hf~ z2YB%ksw1OIS@(G)R8@y>NE|9X+~%zU-%3j@>fWwI<%77?0leDa(AT8j-; zOSyQ|;o5!OdA-tgxPGy7)73yBLQo&v< zZZgs|*OVK z*9L3a8I0zUJ~q4%JlM9S3_W*S?8+<-dV{W)DKd(4Xl_&xro0D^aq}c?h#OhECQ5l-C90;*xDdJic^9L;EEpb)GSd(%QdK-?Pn@so`x8Sa(R?bd@#e zr;Ye{7#86YIT=_OjM!NkU)?_<2o*0QWK)hS;ilP3mN}GuS*0!A_S`eDn*lsHJ7SUk z2og6<4jTPE)9D=AIh{^z(($abO^gT zxAZgcl;spoYraJh+6J8P4b-DbnFbl<^%zOWCn{gQ-P*GG^&+iYXI#7G0J^rn7SLOt zbH?z(!!s~Beu#usN=$=2LEdxaifF6PCv>#>{jpr6b&mT!c2|eioz?v)X=0YiZA-^B z_YlP$PQuFu&NdJ+-&r?y*(fO8G3?&*YJULF+97oJj>chE2k7n}gl=Ws2)DY(3Ol?X zGX)35b2Lj-{Qs45|0jo^gnnNbBKh&(0jzgvY4rBrM%{ttE)srfcBnDhH07tSV_apn z<%WFW&|_!U)yz_iPB5Jqwfm&NngLgZ9+vj`G zscxLASry{hnGY4@Xe(kdvIhKIszn>WvA$y8;qPac!LfSLfO|0^?PoAeUuz}J#uxDn zjJW6~ybFQ}pC+X$PM0UtA669Pu_GR~WERC%ai&mxYb8#t`vNNI_kpKDQ9^yBjv)h28PY-|m1 z2w|9G3gUX}KZ=P(%^4&`19MZzSEEQCA*@i~5oJ|_M)1~$sCwaU>xy=%RiRGK-xKNa zzlO*83Cqcz|2LBF9XG;t|7+iE5vve40(h}{{yzVcc;=C@%f%ZWKJfI)+eufHj^urGAAC{b`~AT zfPQh=Uh#-7|5|M2Pr5dZhd)BF(VaHmqQUP37N0Fz0G7n<--6^!=oo@t^JT1B+sa$b z7jhyPOaMBZikxa_C?)%b$cz+mkj`O%k2f|D?;xA)YPVg}9;1n!N#J7x%YvE}>n~!@ zGvmp;rm-@Niq}MrV#fLt2obcld-I7SI4)?tij=nAPp$W zDoRU#BJKU=eS38JGJctp#$R#^aa_hnzogf86E67AMq#W9e$2gzw;^#+@h3OF<1Zkt z9keE8vgAUqmGH3JVdeV%DfO|{HT;RYwVeCG=`GKSxO@r(9)5^eC9>}CIy`GU znmI>DVAryd&!{i>_X9Eb{N>>;1pTr%aWh5MU(dPN1Z}hvKUhunl9L>IY;72Bs=lzm zcwi*^29cT%U2l(PRS=S@ ztaAdE{Nmw-OIBOLAkoQa#D!J!!>}GG%73ZoSR2Ea+oq?(tgqmH%}fFs;}{KKMr6If zOLr8x<{IcpsZi>fT#5L})yJdCn}%kgErY8hPpumL<>s|9HG`M_#BMizP*z4b7~oSe z8or=@m-cOf$lYxv`t^C;F`D<7z4a@$4x6>Skz=^V-Hd=B- zHXcP|aysqaBbUBras5l%F~C8hY+`l2SZhf9(`!W{6O(K5<|JRktTxXB#Hef*>;L{o z;z=>?73A?>Y4?}`*vwXQm%v#GV0QOGF5JT8bs68;9cgGZB1HS!bM5^3l)FE5I+PT1 zfjWFk=Q$qTSHa)6zjDI_YLluHZJikQpm{KOHf z)E@Ne$?DWPbX%@X0v!B4>bgIDDQoTW8{%Q!P%!9!wr!*M`QLg07HNP-8|)`>KW<6e z4_Xn;e)k3RoRf+Vu%u5Z0y^ypi+sJi^)@rie{`A=uOHWxb^5oY+EniI1*}|@8#%?e z05(<0^t|}-&*l&YuHpR+KrY;!vZnJM-+(h8VC);Vwz%FZ=X(9}F$RUiIVKVTY00&> z*0?6O`EaBQTJkb)jnj^s6beYTvEYUv?6yJO>*O^)w~N1WBiCa&WzQZE53`{D@d^5R ziyvkye$A&iiG`Dm;`GmS&3qYW9{3?1FLQ-0R@Dx=0Z99PP2vR|`zs%OyovFL+ zVga0fDyDvpc79K#9ekeN4uFV|umr!56u+?86Jb$lQBmoCpqR9f5GyGe_&)?ZJRMz~ zg8wBT$uBJWL`Ym(L_%6z?0*G>v^?@}1?d0T!O+vmFVN2CDe%bA)BY(;-NVlLslih_ W$6)X8PycD00qQC`%4LeS5&s7ix^mY5 literal 0 HcmV?d00001 diff --git a/www/assets/stylesheets/images/ui-icons_777620_256x240.png b/www/assets/stylesheets/images/ui-icons_777620_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..d2f58d25581c605a5538fa50408fe09c4a30740d GIT binary patch literal 4676 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 literal 0 HcmV?d00001 diff --git a/www/assets/stylesheets/images/ui-icons_777777_256x240.png b/www/assets/stylesheets/images/ui-icons_777777_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..1d532588b989c5fd03e7a63f7e829b017c35d3cf GIT binary patch literal 7013 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 ZrDX-T^mH1q5`RzvpbBd8WwK_0{|mNJD+vGq literal 0 HcmV?d00001 diff --git a/www/assets/stylesheets/images/ui-icons_cc0000_256x240.png b/www/assets/stylesheets/images/ui-icons_cc0000_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..2825f2004d10b413a76317d47384e6139abcbe00 GIT binary patch literal 4632 zcmeHKXIGP5v%c>nKte|l1Q7@wDbjlhy%zzcyaJ(j0vM{4fKmhn9z_vp0Z~LT6ctc< z6BGpmg(xk6^sY1^BnN%oFX!9&0q0tK?b)-|%$l`l?Q8bTO&cp?CVC!v005XwO$=-S z06aMYTX5*fPBb<9eIj7Kx|X^CP@By_cBeTpkg~C`H~a_y|G)l^87PgmOFzk@C)U== z4&Y{0fWd9rlJhSJUJ66`pm{jtKQb{P1vNSKSWV0Y1o+sUg~a6Sq|hmfX}GR{zQK2@ zhP=&v-vtYGFi z2?u(Z8yf)WH0MlC6gsS_r6C=Oj){gxc`o&g5dg3xnHuQYM}J>;VemZL!#35FDJ<11 z#x3jzMPzhH2uC=pn7ZZa@d%nQ$oUtrK73%iK&7T_Oa=P-?v>7YEeCBbcJEDSFE)K^ z$)D76N;>H^qCLHMA&r=*bG5Zw-8++MGVLnWdW(#``|L(Ai~ zSB)%_odv`MN{{7+m(O{@hGv_=`)c0=pjFh!)0)eLy1L;hxu67knKWln0>Q$ADH1giS0atY1^=R~SM{oO zXn%rDXPFXy;F_NK(3%mhq=q(b6%5if8Ew62CUp)Ct6@}Z;+7Gu0?lw@Z|z@Wt!F{#D=Pn?$lIuxf)>-g28+`Nxao^l_GC_ELqk zjuZJh-TH!-UstN=(mO@cC(smT?D0wbc{T^&FiXGwE0kLgwnZbb704>H@!sRSsnM$0>E%pkfR8q9Gg;*Rd~y5F z>udv~GokTG>qYDg9y2u}JigUI>8I7v*01F)0xIUEl}R!KaI_X_QekA7m4O|-iTlM# z$o&ZMQ2|F=B3kt~o53J>4)PQ)Wbb0J>Q&j)mNaNIUuF{GC8Q&A2@1$M=Y1F^fLt(x|OROO|1yx?f()V@i89 zX=%_I^0Iz7LGAMcAKHq($Vl>8w{>vsRpYY8{cCrN8@ix`RuQOdN(x(iECRe%cu6@-zW!FKF{8Q&r#^vY%W>>7>=_Ae8bR51lyHxl=z1(8 z8KS@f@#x-~$I5R7X0={=h~SuSzD|pZwYAGcNU1AKZ#I;(cPOvPA#arcNMvVI*Okxi zpCw{Eg;`rtnZcA%e)6g@7b}-b+aI2#XWrf^)ajE7p*yI5{#1p0b!T(rGA^Q^XQy?T z6e3VwZd)yGP_3*_{Op}Hd-`zzy##&g#{-#^VF)(CeG=je+w&#@*{)05v6XFXMcgoy zX)@cOE!%1VstsNf)OI!5R?rTzW$k`*a_DtAw?{lTMA8xH#}pIA5#f%2R<+H@doo<|^I~4l7u)1>8tW>jn7kK_^xd*lAjN0batU|Ldv1yPCTJ7V<=P#u zAW!2@HQ7#_X3iJQcz)U>$ytQh{|gzumod$xN8jMrX{|Fh`)W8eU}=2k!G|EkE`v_aX6fRpV%^HZ9+%bw zV+m1CMTAh`y2^AMgDm*++ea#GYC9|)0Gnu&@b<=B!bpT@`5NBzAda#Zw^0Y2fgG0d z5EcN`ImKAb3mJn-Ag1+KrW+6JN>k+RbJ^Zs1g@%vnhIRfarTgWK63g4GW1xT52Ap?B$1~$`ZB6ZOmV~1mD^F1FySA` z&3A;45cKY(-JufzKR6AS04E5LP~`kLa!xOuC$0ex$lO;6v% z_2ANW%I-NE2<;6(RtjCVyU#jgzkd<*2)~{R1c2WXk`6iLxWDjfz&tIodOz8ruQcdH zIs;+?im=dFHM#}Mg;dm{4%9!&hUGIyZE|E+ zYP@!&8MnNpRr$lzt{!U{>2N>gKxx@}BcAZyFVITneXsJUPQM~lXx@0AjBQ$Si${}sl?DZQD@~3xA%$En^0x+AA(q%Dd*fqQNx)N*l1F`jUg7}9tIW%h>C8YX| zoG<*+-%_MWgR|`0TrK5T6Dd-TV|!^G?#=xQqcx;4O3hShTiSnf#P;yjGMepUAXFUk zg>m_;C~4{wi*~MCjSv#06caF|)sc!4@!Z~U zk(M0CjB31q!67laFIe7Y59JtcXk1F$wL_hgJ4Ha2rfO6UTwo~EVIkR7wKY;x3Mry) z6Bu~ToX&2pZ-y07I6$VZ&M;w?Zpa%WIIa!86SJp+eTW6q#e~Nq%sNXF6;)AZu_aj?Fxaa^tB zpUWl}{!rR4@ zBRJU$ZQ8P(ve!%@Y_Y6o$fVUJOE82y#0n{UUvP5hcs?NH(rZDh5qq`9VyNR@_^rIH z?r`Tl-&6Q2()N1Gu4EM^6#Le$N;ZGuuh$!cHYn|{8$4q8TShBIPdw!4&qbRUzOnx& zz)iz$RW2Pz7f{Wt(r90GLfaV;hrIHZlP{T2YWc-u_sKcD4|j`tALDL?N1X$uo4)I>~pI zJB#YSR9|SZlYFlboNu#N0N!OEj!hmy;u+6piqAZNqMzarHIrR3FxK*(mc+|P;C+SU z1xCpi^QO;yKJ-Sc=umetzdT(G~QMe3gE*2ZlIR+e(pEyTzQ<@7w@!r7Ul*wN#J@yYLJ2$B)i4xO1lZ$4@bJo})7 zk?SD4C!cLqBSu;Bpd_N&?^3fpXdeuzRs^db@^%N;{APRenS33f)M%`CxFopbJ#Op2P8O{0di$;4^eR0EF zSa?FIf7CRB&Dgyr-PWQk|FPZ#Ii+Fmg%074>ZCM}-3#KEMgsm)&7hlG zh^{1h6vr0kbvahQj3F}eDTTx~SPTeooPlxpOAO>>+wOkq!|N?L7w6_r%miD-Ef$LC z*Xq~nLM-SlARk^m8SGjAXXP^^EvQ<=j=FBxdIrt+ZW0b2$jXY~9Vt)Moyccqv>mu* zL7_}5_uEWiMTt6S5s0!MD9Kji&Olt_Mb+-Ap}VZX)~VY03KUC?i;lpbzG61Ki^})S z(D={|)X_HZRe78jj=iZZ=FnnHud}cepBhH_@)KgXUNAJM<0rY#Y<^H8jKYt+&$!3t ziEDLwHn`ew_b8PkE9F3rtM?#ybzaZt)xyzh=YKll1gJ{A4elYuz7Fyaexl-;Vppbo zzP|Y_6rc9a&Zc60pBKi#fBXL0SV=V_Vw$Do490<*HSwqLo)7%$~;jFP!zj#9VH zV-j79`x_Sic24ToTlrb8)#WM1-$&8d4a zQQtxcXWco*IlN7F*1ps_<1dnD?4P-_c$?9lb703o{rKL?cyJ0EQFgbS;d^4zbkr7z zg%0&ah?IS2($?x}>MA8`7R`_S=SMxZ`I-+EVU*Zyt#d5=kpd5^o2BsJqhin1@w8Nw zcQxuuwV~;M|6+$EAG7Tyrp4K=v}r@0EGaBW!H5a<7sZLKJ$iARp@L?|4x$2@urC#u z*K*rKtVyg%Q%Dh$g!AUKRu9R#CpVG0>#T)XF}-DFi}Kzt4=57$Kf>No8VO9eQpW?m z&D;Hi-_wQvQSiL1@_HXOqWt>@_n`0h)>q6L?)}Y;prGbc-ZX`~*I0h;L%V*`2m@po zhnu7Bj2LQ2CzgoGq=rm5dJw$?(e~Biifs*ue6Ma$@dj}C^Zvn#K8&1$q4T2+LTVr zSR)*(2;HfrMfBR8(1Xvxh>{k!pR-d%Yh!~)B~8sH7NJ@wN)9?>RZ@mgT|wA93OqDS z`c`q%h*N|gbZ57c&QU`Ul9&mM`fI4ye6ASi$3m`6aNay9})y*;sS`2^pZ`ee}*=UVWop zI2}$UCTEIGbm?fIlHOW%LcHi-*Za#@f!J69M&1A&67cgINE`zdy%Il&ME|chJD~oC z(XZz+`Cs4wmIEZ|Ze;p~j<;xD!4z`@_=QKL9X#PTs6GN>xTPB*o8;ED@PqNq>sZ5+ zmOq~O7U^ox+79UYH~sc!W-qY=lHr~E8FakMCMGjn4&=*C&jz0$p*+Zw@bDY zDWD@Z_dk{lrsRA9{B6&h^e}QaTDi_!XTMzvlXeh!HoFOT+T?;^UpZ^S$r@cu-Yy*r zA%S81R<#CDn=Ge0C1-myXq1Dpo?h~}GF1VFzP;QLeS>;lEN0;r5Jl|Be+YjHdsy8q zf=J*U)yOQobz@qd7h@(&uk1`(u2;J3@ELr_I;UXMZD;U>T_u81{YCi)4-BUnhpJPd z*P5-E?#{=RSa4%$vsl*jr7>G!*k~q<;Gte_2Ho(%cbz6?MtEQ@E#=qy(%di}(ywo( z^&$wPV=dTLn4V(3*yU!nex$WzTgU4*TPKT$=4AHN1>ByNzy~=tJ<>)%(zkYP)bzTJ zE^L1eCjZ}#{AX9R9XPFh(p1zN4oGtZ;a<3Ifl0}1Z{He!u_QBw+$732kFZsPv76@eT5rdFK$*LSV)07oMjCq?@T6L1_+6V>1N=N9f=pT9R{)Y+*Q zE=_Onp8b?JbF*0N2a2^ixzBYlOCHGncaxb~@Khaa%wIT3l|n)Nx5V^}^1)Pc_Ce#g zaVC2ZzKzhUG<~cyBG*UjlG(0O9PfD(IEUQ0+_BdeYx@NHs~NvPH5%R-#(=Y<_z0Q- zCsW<$gH;zHOGqXoN*Q%2Dza)!ubwKrC(P_~Yu${N$FN!k#87~)p7IML=;lXkH)nXu z{1t+~2SAZ`K*uzRci_sw<_tnyUc=L)ElNxcRHCouao=DIxmDyEH-4&%Iz%mE4CO9- zc~tVHQTH*NWrK05BJwEc(C$&Qsb|V=4Wc;dgd3^*hAua61@1D^#&;1s9dGCV?=f8L zdZT_fV+UlLOtgUNZiViKn_iv?#wd|DyO(b49RZAj=*B@&ryo@|d-oi@^zoe*1j2Sl zBXQi<(FSgCl4?c+F8&_0wQs}Ya|feW7j;s<1(W4{uwh%pIiOAb5r$5=N{G}1+Hob>(kHU+ET&jz?npUucnqg3w4TYS5wk{uD& zGMI<6QT?c+Jwu#k#?^AtL6t+FCN?x_AQXwNpZl!-IMGWI&?wX@KVIX%KWA4!nIV^2 z!Z&I1K7%sCpVvw?fXBUfQ#gS0>s_qXGhctXXg_|C5~Yf4*Pdnw#d>;1`B4M?MFLtd z3%E$a{|e>5Qqy+K*OPK=_8+1v4qakc9#c?Ir~sb*+*_uohLpbS&oI; zF|@am1zFl_XFj&Hk$eVyq7EVH$sZ$lgb$b8=KRQ<8ja3# zJv_ZzZz%e6@>{?E*G`!z2E6|-A3Ntb*?M&HY!El|*i|R0HMi?dIlc3Wd~ML;e!*nl zo8&p#_sr$7=VUT z5b^W%S+HRY)3SHs8+~+1=0{s4`Du&N*r?UygS!Tb7H5Xc<9f=!0hG=#TE4P zIJK5C@GhjwM71DMF8`FYzjxE+1?7;GI_NZERU@{L=&|22o}s6Qbg~a@X0MyrecC>{ zMe^gFf=zBQ!^t;s>{eCPN!Ds02^P1g)W*QwEp-pFo7L#hSLmH4w-ID%H2+{R`L^`W zFnYUu5RJzTv69O1Bfi!kv!lkcP7Cer$!Zs!Xk+L|LG<-ue?fe3&$8bD3hUN`@yJ4@ zc_g4FJPqE1ph+qPmkF5ne087u?!}3w!+C%J{Pm1Q+LnhWfcf;*!I@4W@heylj$5{p zSg!H=_1ng8PdE9d?(w%nDWj|*QQyoX0mr`N=0CA7o3F*Y5h?C<2>{0tj*rK8sC;A? z=cYp*9DPcZhnvMn$+LhXk`X1N(qq@=D7LjZ6RM-r2!oju^gL|NSmZ&KL{EiB`I0z< zMfOv^nIrWUyZOT=r4$FJ!$4vw@yyVNo4fU@pwiVP!be_N^_QRCAV8sQ9-eyJZA9!d zgqf)&{-bvML!O2Ata^i5dPo+{%(5AvoEwN}E&zT&X5F-yncrt>XnVmYf)B)_NcgbN8MBs~f(Uj3(7cOVr?^l=sT@9Rx!T zc)K1jv&EhAkgoq!q>f+|31sY3^4t{U>1vN)2k>ov%Kdb^+Y06Lkl8w;m5oI}`%>T# z0gB+jX7lvb688egALlrcUWuT5EW408BJu3c;8ldewN^4ql)voC^^v&_k!?EwCp0|( z-puh>*R>7nI*O$q87JKh0G52uk;P{%!YiWNAG(UHbgjg{!@~3n4w%pjASBN#ERUmh z`mM3TM;dGF4|AkX@;|}9pIUjwq@rVs7iD@+DR-U8oDqqQ%B|hF1-|0rn&rGf zHIg8@z;cSu@6fHeFKTz8d}EA+<7e@>*vPQ3>DFifTTG0Z3lfmGqDSuBG~~hVUmDGL z{V|TNW&*9m!y9q!r4(--+5Z5Yzf;Y#CzgM_swq`K=JdAI)tk8^FNXJq^XR_jw-liM=q0L#Jh6FN4=(2EApeLKAcTwc9zU zkxAY*Nr~llO1x)Q+=g_|0G!u3)9-T7_Y`05Xa!>0!WUuwNoNYJr}^A->d7fjtcpUd zbCN$%rqHaVzXY+61{EvYrr5-LWF})IT|?#s`17KiH(4JU2&m}uu2Svp$ka4N=Go^5 zN}JH~A1@NOUe`W6PBn)8ve|8a`LGYClZy}zBDzigv~-Gm z>*g+>H~wH)PkqLXeX`|3I@g_YsPFyW=UTLO>bgssN{syu(_Qu-tiaOw`d;|^(}mW- zI;HhMa-ijsqYzf@-66dzUBD>2@z&@-mpXE?^?M}JTW7aOXIE@W>sZHUoyU@H+Y)@t z9ou4TXmpHLjYMkE-tyZ5=~@5oH^jldgvM#+6QYe%qLYeSgG0)*n=*7XM6^C1cWWXC zM=FL_e!Quo2;fm&MXx_pkb}9`B;7oNF*t;}%rNRG|KL<;{I$441@Pmin zsM3E>>Hlu(`5}!(_kGTV_4Z`vp$S*8zQ?F+XUc#iy}gE(FU>S1bMb_O+>rv;??hp; zIxlm4jh~f?&R8&e(KGmx9Clr%G7sjJa0)*a^J!vzpwjb;;Y4T>$p;dno^8As@}JWU(R&#d$s&w`5tJ6PMH#?<}r6Un;e8xx6Kx z{zwkqFvEW#9D-F@o3z{2C^E7^DJ{{Q72DxEbc$Q~^G+GZ+7Vcx1zf|^f?;|(?=pm^=L77+dr4z?*`IRg*+Dmm`S#ydsTc# z+sn|kLrVk-cili!n9!Ivr0exH>lN0)acF;4T>$K??bmGK0i zXs5cU@7!%8@BA|3u(BbYV+9t&ZfYQ@vF?tl=ThQW`&QG(W7XEz`>-VQ0sw9> zgT{?gd{Rw(hl~yFxk9BOvvdr9YjgNi##?-cSzvnyy^HlUzVd#Yx@q)3ZfHJ@G| z#2Ay|j`!k=tEGi|bf{UXzMdhar|E0g0_N^2Ns#pDm((0b-H#lop7?QF#pbL!dpX%$ z%ugi6dRl78y;x+7ksrS3Vt$;J_Ik^4wKv zgZM4`%ij2dJ#^h1ATN&+gw6E_G45pB@LquS$1BvRp}*kfMHz@UK)wydZud3V&6>@! z*6Spti+IpM!cnMI+8to0a8kPm^Nvu-hL+)3L%ZKxgt~y6OV!m`8~R131@hFrgEaOz zlVkN!iyHgP$6LngicGvXE-|=?8_%|iArjB=ga9-1WpDF}c`o>S*`l0YQhC^3*|w&; z_mrTaufMm)F@TG_D1^63kT}Wd(oF2E>3eVQ2c%6}iKtai>%5|}%fy@Yx!|7i^Cx(# zEz;eGhU1mSb*@e&N8ub_a*0b)Op=SdityJ=u_+o^h$!o!N|Ayp9GE?sY8*HN&#U^| zjYf9~Sawur%wd45-%r@Z^uMj29YexMb`w0%`yOaG5fU7f&f*y3e-pspVlVbP>)*KW zpB7yuPBGTv#FPaa>>B4^2|gsx9~vk9Mq5jy1DBy6~(z#XX7mK8rWIoy*gDlziks2rbdX#)mgs*!6WWE_ljEH85=e(=NG+SckN z$==Fk8$L<`_7D9?D<>Cd(!spToeK=0Tz=SV|5uT$?h;X(?%Mizi4jzzjwX^he)*>~ z`bV-nfn4&(dIVKGw^XWhJX>87L(is5n*O(B{5M*LQ{`BceV&hl?bK?Z*l!`*q~7M3 z1GEJi7rKjXGSt=MvSiCu5~ZH-sw8LBhc{GRQ5S|)k$ zVxo=LCOu?B2?Qw7%}W5dJ^)!?8M{+kW~5k|U=smC|A5f`A@P%2^MV$Js8Fp545610 zRENu!MFgyA?40cx10dcAcdhDL7fI-6OJ1O-OJ(Dl=_bEhlNCT+pwwY!+eT~eD4s#c zT3c@(Kabl#nM?c}BqD*X~!0#*`4Hvia~d(L6V?En>)Uz)OB%TfwvkT;RDqX^l&EpV&8(c=HI7I>OYt; zF_MN^iXn>_2F064G`4DCM+JR~z7K4cRP44$SH;48WfeXDd9*HY(qP0{asUc>)vWOP zO$~fxIkWCl`wKggY;o1cIvlN0&yU``LXRa{1TBMGZ3_85#^!p-i58!ZEZgcq9+1q7Ar)(67e}|4=E@vshUH>N zVq!Vu4~?IuH4*7R0K%yZyRacVdr=^Zi@`&4x^{y|L)uR$MZmUm-I;7ktfx%FsrKkH zuFOFgc5rOayR*5(wq5RgRz*Lr6V3vGnx83BC`W-g)`E5wE;BAy8^Z~x^`Ml~7rf2L zlh;Sx4+9-Y^Z>v7Q=!}_Hvd5fJMCsk+mS8&qlQRPn`5>B2ibG>NaTj_w*1}w%R9y| z9moQx>k0lPL>eJh8TUD}0!d{Z<$I0Uh?1-e7}v1myIv&9Q=5kUkdb3rd-Y+&rObVo z)q>@$ZV)Si*KLyYR!Um9FtEx$Bl>DC_gQVO-7@A^*j4PnC1)dR=r4k?%`5@Mq;-LJ z$p1gWt3>ww?~pN>bpncY=yPn}a=m~;h~q7%7!W!@ zuon|puuB@+aOu5C!U9Yp`jmk+9ej^mhg8zVDwW5{N{~`BTlz(mXeny_d3WNmkYnQ$ zR`t5lw>BrC45E2_y9Z<*FRi~>Vf1TYe=SIfp7~!q>c2y`UkVf%2=m|^q8f^RlQcx} zR^t!+pb|ZvP{as?kd*#JjK+Nkb^oc69;UzsQUUGnV1KoZH{~C7Q_gWChd!LjlU9o* z=(^df(opYoM~r#|sJPG=UkV(7O(U1V8+IbEgo>vxCV6I5H{ zU&jo_Ztn&&j}Guik+|Tl?E%BY+em-9A+FyC;c~KwK}N|u26z;55joZz@DqIixFDf? z1@WZ2ZXbgSKaDDC2&nLXrG#+qtW%@dsn2-gG|k?99p4GRp!J19bnk`ScM5S;aS3w0 z7=Y{2G71vX$`UeicVuK$WMx(U66I8+rTJ)RN&Z8@*Z;n!+k^jBP?V67y(2BJa$P}1 zUhW?SX(Qja7YZDI{o%I1TS&N5kSm~h-{0Al*TC1w-PPRH>HdSjFRp4A9{~osCOYL> Hjt~C}Fe}Pt literal 0 HcmV?d00001 diff --git a/www/index.html b/www/index.html index 7a86765..253c35d 100644 --- a/www/index.html +++ b/www/index.html @@ -112,7 +112,15 @@

-
+
+
+
+
+
+ + +
+
-- 2.39.2