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

Port Websocket server from gz-launch #2792

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .github/ci/packages.apt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ libprotobuf-dev
libprotoc-dev
libsdformat15-dev
libtinyxml2-dev
libwebsockets-dev
libxi-dev
libxmu-dev
libpython3-dev
Expand Down
12 changes: 12 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,18 @@ set(GZ_TOOLS_VER 2)
gz_find_package(gz-utils3 REQUIRED COMPONENTS cli)
set(GZ_UTILS_VER ${gz-utils3_VERSION_MAJOR})


#--------------------------------------
# Find libwebsockets
# Disable on windows as there is an issue linking against websockets_shared,
# see #2802
if (NOT WIN32)
gz_find_package(libwebsockets)
if (NOT libwebsockets_FOUND)
gz_build_warning("Unable to find libwebsockets. The websocket_server system will not be built.")
endif()
endif()

#--------------------------------------
# Find protobuf
gz_find_package(GzProtobuf
Expand Down
72 changes: 72 additions & 0 deletions examples/scripts/websocket_server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Get Started

This demo shows how to launch Gazebo with a websocket server for
web visualization with [gzweb](https://github.com/gazebo-web/gzweb).

1. Launch the `websocket_server.sdf` demo world. The demo world is no different
from other Gazebo worlds except it includes the
`gz-sim-websocket-server-system`.

```bash
gz sim -v 4 -s websocket_server.sdf
```
1. View Gazebo simulation in the web

1. Option 1: Open `index.html` in a web browser.

```bash
firefox index.html
```
* The `index.html` web page is a simple demo that integrates a snapshot
version of gzweb for communicating with the local websocket server.
It illustrates how gzweb connects to the websocket server
the events that it listens to in order to create the scene. For a more
up-to-date gzweb visualization, see Option 2.

1. Option 2: In a web browser, go to https://app.gazebosim.org/visualization,
and connect to the local websocket server on `ws://localhost:9002`.

# Authorization

The `websocket_server` plugin accepts to authentication keys:

* `<authorization_key>` : If this is set, then a connection must provide the
matching key using an "auth" call on the websocket. If the `<admin_authorization_key>` is set, then the connection can provide that key.

* `<admin_authorization_key>` : If this is set, then a connection must provide the matching key using an "auth" call on the websocket. If the `<authorization_key>` is set, then the connection can provide that key.

Two keys are used in order to support authorization of different users.
A competition scenario may require admin access while prohibiting user
access.

# SSL

1. Use the `localhost.cert` and `localhost.key` files for testing purposes.
Configure the websocket server system using:

```xml
<ssl>
<cert_file>PATH_TO_localhost.cert</cert_file>
<private_key_file>PATH_TO_localhost.key</private_key_file>
</ssl>
```

* You can create your own self-signed certificates using the following
command. Use "localhost" for the "Common Name" question.

```bash
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.cert
```

2. Run gz-sim with the websocket-server system

3. Run a browser, such as firefox, with the `index.html` file.

```bash
firefox index.html
```

4. Open another browser tab, and go to `https://localhost:9002`. Accept the
certificate.

5. Refresh the `index.html` browser tab.
1 change: 1 addition & 0 deletions examples/scripts/websocket_server/eventemitter2.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

210 changes: 210 additions & 0 deletions examples/scripts/websocket_server/gz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* Copyright (C) 2020 Open Source Robotics Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

// This file was ported from:
// https://github.com/gazebosim/gz-launch/blob/main/plugins/websocket_server
// Part of the code is taken from gzweb:
// https://github.com/gazebo-web/gzweb.

/// \brief Construct a complete websocket message.
/// \param[in] _frameParts This must be an array of four strings:
/// 1. operation,
/// 2. topic name,
/// 3. message type, and
/// 4. payload
function buildMsg(_frameParts) {
return _frameParts.join(',');
}

/// \brief The main interface to the Gazebo websocket server and
/// data on Gazebo Transport.
function Gazebo(options) {
options = options || {};

this.socket = null;
this.topics = [];
this.worlds = [];
this.isConnected = false;

// Start with a null root protobuf object. This object will be
// created when we get the set of protobuf definitions from the server.
this.root = null;

if (options.url) {
this.connect(options.url, options.key);
}
}
Gazebo.prototype.__proto__ = EventEmitter2.prototype;

/// \brief Connect to the specified WebSocket.
/// \param url - WebSocket URL for Gazebo HTTPServer
Gazebo.prototype.connect = function(url, key) {
var that = this;

/// \brief Emits a 'connection' event on WebSocket connection.
/// \param event - the argument to emit with the event.
function onOpen(event) {
that.socket.send(buildMsg(['auth','','',key]));
}

/// \brief Emits a 'close' event on WebSocket disconnection.
/// \param event - the argument to emit with the event.
function onClose(event) {
that.isConnected = false;
that.emit('close', event);
}

/// \brief 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);
}

/// \brief Parses message responses from Gazebo and sends to the
/// appropriate topic.
// \param message - the JSON message from the Gazebo
// httpserver.
function onMessage(_message) {
if (that.root === undefined || that.root === null) {
// Read the Blob as an array buffer
var f = new FileReader();

f.onloadend = function(event) {
// This is the proto message data
var contents = event.target.result;
if (contents == "authorized") {
that.socket.send(buildMsg(["protos",'','','']));
}
else if (contents !== "invalid") {
that.root = protobuf.parse(contents, {keepCase: true}).root;
that.isConnected = true;
that.emit('connection', event);

// Request the list of topics on start.
that.socket.send(buildMsg(['topics','','','']));

// Request the list of worlds on start.
// \todo Switch this to a service call when this issue is
// resolved:
// https://github.com/gazebosim/gz-transport/issues/135
that.socket.send(buildMsg(['worlds','','','']));
}
};

// Read the blob data as an array buffer.
f.readAsText(_message.data);
return;
}

var f = new FileReader();
f.onloadend = function(event) {
// Decode as UTF-8 to get the header
var str = new TextDecoder("utf-8").decode(event.target.result);
const frameParts = str.split(',');
var msgType = that.root.lookup(frameParts[2]);
var buf = new Uint8Array(event.target.result);

// Decode the message. The "+3" in the slice accounts for the commas
// in the frame.
var msg = msgType.decode(
buf.slice(frameParts[0].length + frameParts[1].length +
frameParts[2].length+3));

// Handle the topic list special case.
if (frameParts[1] == 'topics') {
that.topics = msg.data;
} else if (frameParts[1] == 'scene') {
that.emit('scene', msg);
} else if (frameParts[1] == 'worlds') {
that.worlds = msg.data;
that.emit('worlds', that.worlds);

// Request the scene for the first world.
// that.socket.send(buildMsg(['scene',that.worlds[0],'','']));
} else {
// This will pass along the message on the appropriate topic.
that.emit(frameParts[1], msg);
}
}
// Read the blob data as an array buffer.
f.readAsArrayBuffer(_message.data);
}

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

/// \brief Send a message to the websocket server
/// \param[in] _msg Message to send
Gazebo.prototype.sendMsg = function(_msg) {
var that = this;

var emitter = function(msg){
that.socket.send(msg);
};

// Wait for a connection before sending the message.
if (!this.isConnected) {
that.on('connection', function() {
emitter(_msg);
});
} else {
emitter(_msg);
}
};

/// \brief Interface to Gazebo Transport topics.
function Topic(options) {
options = options || {};
this.gz = options.gz;
this.name = options.name;
this.messageType = options.messageType;
this.isAdvertised = false;

// Subscribe immediately if the callback is specified.
if (options.callback) {
this.subscribe(options.callback);
}
}
Topic.prototype.__proto__ = EventEmitter2.prototype

// \brief Every time a message is published for the given topic, the callback
// will be called with the message object.
// \param[in] callback - function with the following params:
// * message - the published message
Topic.prototype.subscribe = function(_callback) {
var that = this;

var emitter = function(_cb) {
// Register the callback with the topic name
that.gz.on(that.name, _cb);

// Send the subscription message over the websocket.
that.gz.sendMsg(buildMsg(['sub', that.name, '', '']));
}

if (!this.gz.isConnected) {
this.gz.on('connection', function() {
emitter(_callback);
});
} else {
emitter(_callback);
}
};
Loading