Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wip] TCP connections to ROS bridge for node #117

Merged
merged 1 commit into from
Oct 6, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
});
});
});