/**
* @fileOverview
* @author Brandon Alexander - baalexander@gmail.com
*/
var WebSocket = require('ws');
var WorkerSocket = require('../util/workerSocket');
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.
* * <topicName> - A message came from rosbridge with the given topic name.
* * <serviceID> - A service response came from rosbridge with the given ID.
*
* @constructor
* @param {Object} options
* @param {string} [options.url] - The WebSocket URL for rosbridge or the node server URL to connect using socket.io (if socket.io exists in the page). Can be specified later with `connect`.
* @param {boolean} [options.groovyCompatibility=true] - Don't use interfaces that changed after the last groovy release or rosbridge_suite and related tools.
* @param {string} [options.transportLibrary=websocket] - One of 'websocket', 'workersocket', 'socket.io' or RTCPeerConnection instance controlling how the connection is created in `connect`.
* @param {Object} [options.transportOptions={}] - The options to use when creating a connection. Currently only used if `transportLibrary` is RTCPeerConnection.
*/
function Ros(options) {
options = options || {};
var that = this;
this.socket = null;
this.idCounter = 0;
this.isConnected = false;
this.transportLibrary = options.transportLibrary || 'websocket';
this.transportOptions = options.transportOptions || {};
this._sendFunc = function(msg) { that.sendEncodedMessage(msg); };
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 {string} url - WebSocket URL or RTCDataChannel label for rosbridge.
*/
Ros.prototype.connect = function(url) {
if (this.transportLibrary === 'socket.io') {
this.socket = assign(io(url, {'force new connection': true}), socketAdapter(this));
this.socket.on('connect', this.socket.onopen);
this.socket.on('data', this.socket.onmessage);
this.socket.on('close', this.socket.onclose);
this.socket.on('error', this.socket.onerror);
} else if (this.transportLibrary.constructor.name === 'RTCPeerConnection') {
this.socket = assign(this.transportLibrary.createDataChannel(url, this.transportOptions), socketAdapter(this));
} else if (this.transportLibrary === 'websocket') {
if (!this.socket || this.socket.readyState === WebSocket.CLOSED) {
var sock = new WebSocket(url);
sock.binaryType = 'arraybuffer';
this.socket = assign(sock, socketAdapter(this));
}
} else if (this.transportLibrary === 'workersocket') {
this.socket = assign(new WorkerSocket(url), socketAdapter(this));
} else {
throw 'Unknown transportLibrary: ' + this.transportLibrary.toString();
}
};
/**
* Disconnect from the WebSocket server.
*/
Ros.prototype.close = function() {
if (this.socket) {
this.socket.close();
}
};
/**
* Send an authorization request to the server.
*
* @param {string} mac - MAC (hash) string given by the trusted source.
* @param {string} client - IP of the client.
* @param {string} dest - IP of the destination.
* @param {string} rand - Random string given by the trusted source.
* @param {Object} t - Time of the authorization request.
* @param {string} level - User level as a string given by the client.
* @param {Object} 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);
};
/**
* Send an encoded message over the WebSocket.
*
* @param {Object} messageEncoded - The encoded message to be sent.
*/
Ros.prototype.sendEncodedMessage = function(messageEncoded) {
var emitter = null;
var that = this;
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(messageEncoded);
});
} else {
emitter(messageEncoded);
}
};
/**
* Send the message over the WebSocket, but queue the message up if not yet
* connected.
*
* @param {Object} message - The message to be sent.
*/
Ros.prototype.callOnConnection = function(message) {
if (this.transportOptions.encoder) {
this.transportOptions.encoder(message, this._sendFunc);
} else {
this._sendFunc(JSON.stringify(message));
}
};
/**
* Send a set_level request to the server.
*
* @param {string} level - Status level (none, error, warning, info).
* @param {number} [id] - 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);
};
/**
* Retrieve a list of action servers in ROS as an array of string.
*
* @param {function} callback - Function with the following params:
* @param {string[]} callback.actionservers - Array of action server names.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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);
});
}
};
/**
* Retrieve a list of topics in ROS as an array.
*
* @param {function} callback - Function with the following params:
* @param {Object} callback.result - The result object with the following params:
* @param {string[]} callback.result.topics - Array of topic names.
* @param {string[]} callback.result.types - Array of message type names.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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);
});
}
};
/**
* Retrieve a list of topics in ROS as an array of a specific type.
*
* @param {string} topicType - The topic type to find.
* @param {function} callback - Function with the following params:
* @param {string[]} callback.topics - Array of topic names.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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);
});
}
};
/**
* Retrieve a list of active service names in ROS.
*
* @param {function} callback - Function with the following params:
* @param {string[]} callback.services - Array of service names.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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);
});
}
};
/**
* Retrieve a list of services in ROS as an array as specific type.
*
* @param {string} serviceType - The service type to find.
* @param {function} callback - Function with the following params:
* @param {string[]} callback.topics - Array of service names.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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);
});
}
};
/**
* Retrieve the details of a ROS service request.
*
* @param {string} type - The type of the service.
* @param {function} callback - Function with the following params:
* @param {Object} callback.result - The result object with the following params:
* @param {string[]} callback.result.typedefs - An array containing the details of the service request.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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);
});
}
};
/**
* Retrieve the details of a ROS service response.
*
* @param {string} type - The type of the service.
* @param {function} callback - Function with the following params:
* @param {Object} callback.result - The result object with the following params:
* @param {string[]} callback.result.typedefs - An array containing the details of the service response.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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);
});
}
};
/**
* Retrieve a list of active node names in ROS.
*
* @param {function} callback - Function with the following params:
* @param {string[]} callback.nodes - Array of node names.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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);
});
}
};
/**
* Retrieve a list of subscribed topics, publishing topics and services of a specific node.
* <br>
* These are the parameters if failedCallback is <strong>defined</strong>.
*
* @param {string} node - Name of the node.
* @param {function} callback - Function with the following params:
* @param {string[]} callback.subscriptions - Array of subscribed topic names.
* @param {string[]} callback.publications - Array of published topic names.
* @param {string[]} callback.services - Array of service names hosted.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*
* @also
*
* Retrieve a list of subscribed topics, publishing topics and services of a specific node.
* <br>
* These are the parameters if failedCallback is <strong>undefined</strong>.
*
* @param {string} node - Name of the node.
* @param {function} callback - Function with the following params:
* @param {Object} callback.result - The result object with the following params:
* @param {string[]} callback.result.subscribing - Array of subscribed topic names.
* @param {string[]} callback.result.publishing - Array of published topic names.
* @param {string[]} callback.result.services - Array of service names hosted.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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);
});
}
};
/**
* Retrieve a list of parameter names from the ROS Parameter Server.
*
* @param {function} callback - Function with the following params:
* @param {string[]} callback.params - Array of param names.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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);
});
}
};
/**
* Retrieve the type of a ROS topic.
*
* @param {string} topic - Name of the topic.
* @param {function} callback - Function with the following params:
* @param {string} callback.type - The type of the topic.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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);
});
}
};
/**
* Retrieve the type of a ROS service.
*
* @param {string} service - Name of the service.
* @param {function} callback - Function with the following params:
* @param {string} callback.type - The type of the service.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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);
});
}
};
/**
* Retrieve the details of a ROS message.
*
* @param {string} message - The name of the message type.
* @param {function} callback - Function with the following params:
* @param {string} callback.details - An array of the message details.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
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 typedef array into a dictionary like `rosmsg show foo/bar`.
*
* @param {Object[]} defs - Array of type_def dictionary.
*/
Ros.prototype.decodeTypeDefs = function(defs) {
var that = this;
var decodeTypeDefsRec = function(theType, hints) {
// calls itself recursively to resolve type definition using 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) {
typeDefDict[fieldName] = subResult; // add this decoding result to dictionary
}
else {
typeDefDict[fieldName] = [subResult];
}
}
else {
that.emit('error', 'Cannot find ' + fieldType + ' in decodeTypeDefs');
}
}
}
return typeDefDict;
};
return decodeTypeDefsRec(defs[0], defs);
};
/**
* Retrieve a list of topics and their associated type definitions.
*
* @param {function} callback - Function with the following params:
* @param {Object} callback.result - The result object with the following params:
* @param {string[]} callback.result.topics - Array of topic names.
* @param {string[]} callback.result.types - Array of message type names.
* @param {string[]} callback.result.typedefs_full_text - Array of full definitions of message types, similar to `gendeps --cat`.
* @param {function} [failedCallback] - The callback function when the service call failed with params:
* @param {string} failedCallback.error - The error message reported by ROS.
*/
Ros.prototype.getTopicsAndRawTypes = function(callback, failedCallback) {
var topicsAndRawTypesClient = new Service({
ros : this,
name : '/rosapi/topics_and_raw_types',
serviceType : 'rosapi/TopicsAndRawTypes'
});
var request = new ServiceRequest();
if (typeof failedCallback === 'function'){
topicsAndRawTypesClient.callService(request,
function(result) {
callback(result);
},
function(message){
failedCallback(message);
}
);
}else{
topicsAndRawTypesClient.callService(request, function(result) {
callback(result);
});
}
};
module.exports = Ros;