Skip to content

Commit

Permalink
TCP connections to ROS bridge for node
Browse files Browse the repository at this point in the history
  • Loading branch information
gyeates committed Oct 2, 2014
1 parent b1242c3 commit e7bc931
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 107 deletions.
3 changes: 3 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ module.exports = function(grunt) {
},
examples: {
src: ['./test/examples/*.js']
},
tcp: {
src: ['./test/tcp/*.js']
}
},
uglify: {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "roslibjs",
"main": "./src/RosLib.js",
"main": "./src/RosLibNode.js",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-browserify": "^3.0.1",
Expand Down Expand Up @@ -37,6 +37,7 @@
"scripts": {
"test": "grunt test",
"test-examples": "grunt mochaTest:examples && karma start test/examples/karma.conf.js",
"test-tcp": "grunt mochaTest:tcp",
"publish": "grunt build"
},
"repository": {
Expand Down
8 changes: 8 additions & 0 deletions src/RosLibNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* ROSLIB Node exclusive extensions
*/
var assign = require('object-assign');

module.exports = assign(require('./RosLib'), {
Ros: require('./node/RosTCP.js')
});
114 changes: 8 additions & 106 deletions src/core/Ros.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
* @author Brandon Alexander - baalexander@gmail.com
*/

var Canvas = require('canvas');
var Image = Canvas.Image || global.Image;
var EventEmitter2 = require('eventemitter2').EventEmitter2;
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.
Expand All @@ -27,16 +27,16 @@ var ServiceRequest = require('./ServiceRequest');
*/
function Ros(options) {
options = options || {};
var url = options.url;
this.socket = null;
this.idCounter = 0;
this.isConnected = false;

// Sets unlimited event listeners.
this.setMaxListeners(0);

// begin by checking if a URL was given
if (url) {
this.connect(url);
if (options.url) {
this.connect(options.url);
}
}

Expand All @@ -48,105 +48,7 @@ Ros.prototype.__proto__ = EventEmitter2.prototype;
* @param url - WebSocket URL for Rosbridge
*/
Ros.prototype.connect = function(url) {
var that = this;

/**
* Emits a 'connection' event on WebSocket connection.
*
* @param event - the argument to emit with the event.
*/
function onOpen(event) {
that.emit('connection', event);
}

/**
* Emits a 'close' event on WebSocket disconnection.
*
* @param event - the argument to emit with the event.
*/
function onClose(event) {
that.emit('close', event);
}

/**
* Emits an 'error' event whenever there was an error.
*
* @param event - the argument to emit with the event.
*/
function onError(event) {
that.emit('error', event);
}

/**
* 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.
*
* @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;

// 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]);
}
var decompressedData = JSON.parse(jsonData);
callback(decompressedData);
};
// Sends the image data to load.
image.src = 'data:image/png;base64,' + data.data;
}

/**
* Parses message responses from rosbridge and sends to the appropriate
* topic, service, or param.
*
* @param message - the raw JSON message from rosbridge.
*/
function onMessage(message) {
function handleMessage(message) {
if (message.op === 'publish') {
that.emit(message.topic, message.msg);
} else if (message.op === 'service_response') {
that.emit(message.id, message);
}
}

var data = JSON.parse(message.data);
if (data.op === 'png') {
decompressPng(data, function(decompressedData) {
handleMessage(decompressedData);
});
} else {
handleMessage(data);
}
}

this.socket = new WebSocket(url);
this.socket.onopen = onOpen;
this.socket.onclose = onClose;
this.socket.onerror = onError;
this.socket.onmessage = onMessage;
this.socket = assign(new WebSocket(url), socketAdapter(this));
};

/**
Expand Down Expand Up @@ -193,7 +95,7 @@ Ros.prototype.callOnConnection = function(message) {
var that = this;
var messageJson = JSON.stringify(message);

if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
if (!this.isConnected) {
that.once('connection', function() {
that.socket.send(messageJson);
});
Expand Down
114 changes: 114 additions & 0 deletions src/core/SocketAdapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* 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
*/
'use strict';

var Canvas = require('canvas');
var Image = Canvas.Image || global.Image;
var WebSocket = require('ws');

/**
* 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.
*
* @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;

// 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.data;
}

/**
* 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.
*/
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);
}
}

return {
/**
* Emits a 'connection' event on WebSocket connection.
*
* @param event - the argument to emit with the event.
*/
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.
*/
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.
*/
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.
*/
onmessage: function onMessage(message) {
var data = JSON.parse(typeof message === 'string' ? message : message.data);
if (data.op === 'png') {
decompressPng(data, handleMessage);
} else {
handleMessage(data);
}
}
};
}

module.exports = SocketAdapter;
54 changes: 54 additions & 0 deletions src/node/RosTCP.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
var Ros = require('../core/Ros');
var net = require('net');
var socketAdapter = require('../core/SocketAdapter.js');
var util = require('util');

/**
* Same as core Ros except supports TCP connections
*/
function RosTCP(options) {
options = options || {};
if (!options.encoding) {
util.debug('ROSLib uses utf8 encoding by default.' +
'It would be more efficent to use ascii (if possible)');
}
this.encoding = options.encoding || 'utf8';
Ros.call(this, options);

if (!this.socket && (options.host || options.port)) {
this.connect({
host: options.host,
port: options.port
});
}
}

util.inherits(RosTCP, Ros);

/**
* Connects to a live socket
*
* * url (String|Int|Object): Address and port to connect to (see http://nodejs.org/api/net.html)
* format {host: String, port: Int} or (port:Int), or "host:port"
*/
RosTCP.prototype.connect = function(url) {
if (typeof url === 'string' && url.slice(0, 5) === 'ws://') {
Ros.prototype.connect.call(this, url);
} else {
var events = socketAdapter(this);
this.socket = net.connect(url)
.on('data', events.onmessage)
.on('close', events.onclose)
.on('error', events.onerror)
.on('connect', events.onopen);
this.socket.setEncoding(this.encoding);
this.socket.setTimeout(0);

// Little hack for call on connection
this.socket.send = this.socket.write;
// Similarly for close
this.socket.close = this.socket.end;
}
};

module.exports = RosTCP;
24 changes: 24 additions & 0 deletions test/tcp/check-topics.examples.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
var expect = require('chai').expect;
var ROSLIB = require('../..');

var expectedTopics = [
// '/turtle1/cmd_vel', '/turtle1/color_sensor', '/turtle1/pose',
// '/turtle2/cmd_vel', '/turtle2/color_sensor', '/turtle2/pose',
'/tf2_web_republisher/status', '/tf2_web_republisher/feedback',
// '/tf2_web_republisher/goal', '/tf2_web_republisher/result',
'/fibonacci/feedback', '/fibonacci/status', '/fibonacci/result'
];

describe('Example topics are live', function(done) {
it('getTopics', function(done) {
var ros = new ROSLIB.Ros({
port: 9090
});
ros.getTopics(function(topics) {
expectedTopics.forEach(function(topic) {
expect(topics).to.contain(topic, 'Couldn\'t find topic: ' + topic);
});
done();
});
});
});

0 comments on commit e7bc931

Please sign in to comment.