Skip to content
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
14 changes: 13 additions & 1 deletion rosboard/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import uuid

from . import __version__
from plugins import get_plugin_js_files, received_plugin_message

class NoCacheStaticFileHandler(tornado.web.StaticFileHandler):
def set_extra_headers(self, path):
Expand Down Expand Up @@ -49,6 +50,7 @@ def open(self):
self.write_message(json.dumps([ROSBoardSocketHandler.MSG_SYSTEM, {
"hostname": socket.gethostname(),
"version": __version__,
"plugin_js_files": get_plugin_js_files()
}], separators=(',', ':')))

def on_close(self):
Expand Down Expand Up @@ -142,7 +144,7 @@ def on_message(self, message):
self.latency = (received_pong_time - self.last_ping_times[argv[1].get(ROSBoardSocketHandler.PONG_SEQ, 0) % 1024]) / 2
if self.latency > 1000.0:
self.node.logwarn("socket %s has high latency of %.2f ms" % (str(self.id), self.latency))

if self.latency > 10000.0:
self.node.logerr("socket %s has excessive latency of %.2f ms; closing connection" % (str(self.id), self.latency))
self.close()
Expand Down Expand Up @@ -187,13 +189,23 @@ def on_message(self, message):
except KeyError:
print("KeyError trying to remove sub")

# client wants to send something to a plugin
elif argv[0] == ROSBoardSocketHandler.MSG_PLUGIN_MESSAGE:
if len(argv) != 2 or type(argv[1]) is not dict:
print("error: plugin_message: bad: %s" % message)
return
name = argv[1].get("name")
msg = argv[1].get("message")
received_plugin_message(name, msg)

ROSBoardSocketHandler.MSG_PING = "p";
ROSBoardSocketHandler.MSG_PONG = "q";
ROSBoardSocketHandler.MSG_MSG = "m";
ROSBoardSocketHandler.MSG_TOPICS = "t";
ROSBoardSocketHandler.MSG_SUB = "s";
ROSBoardSocketHandler.MSG_SYSTEM = "y";
ROSBoardSocketHandler.MSG_UNSUB = "u";
ROSBoardSocketHandler.MSG_PLUGIN_MESSAGE = "pm";

ROSBoardSocketHandler.PING_SEQ = "s";
ROSBoardSocketHandler.PONG_SEQ = "s";
Expand Down
59 changes: 30 additions & 29 deletions rosboard/html/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,6 @@ var snackbarContainer = document.querySelector('#demo-toast-example');

let subscriptions = {};

if(window.localStorage && window.localStorage.subscriptions) {
if(window.location.search && window.location.search.indexOf("reset") !== -1) {
subscriptions = {};
updateStoredSubscriptions();
window.location.href = "?";
} else {
try {
subscriptions = JSON.parse(window.localStorage.subscriptions);
} catch(e) {
console.log(e);
subscriptions = {};
}
}
}

let $grid = null;
$(() => {
$grid = $('.grid').masonry({
Expand Down Expand Up @@ -75,13 +60,6 @@ function newCard() {
return card;
}

let onOpen = function() {
for(let topic_name in subscriptions) {
console.log("Re-subscribing to " + topic_name);
initSubscribe({topicName: topic_name, topicType: subscriptions[topic_name].topicType});
}
}

let onSystem = function(system) {
if(system.hostname) {
console.log("hostname: " + system.hostname);
Expand All @@ -92,6 +70,30 @@ let onSystem = function(system) {
console.log("server version: " + system.version);
versionCheck(system.version);
}

for (let idx in system.plugin_js_files) {
importJsOnce(system.plugin_js_files[idx]);
}

if(window.localStorage && window.localStorage.subscriptions) {
if(window.location.search && window.location.search.indexOf("reset") !== -1) {
subscriptions = {};
updateStoredSubscriptions();
window.location.href = "?";
} else {
try {
subscriptions = JSON.parse(window.localStorage.subscriptions);
} catch(e) {
console.log(e);
subscriptions = {};
}
}
}

for(let topic_name in subscriptions) {
console.log("Re-subscribing to " + topic_name);
initSubscribe({topicName: topic_name, topicType: subscriptions[topic_name].topicType});
}
}

let onMsg = function(msg) {
Expand All @@ -108,7 +110,7 @@ let currentTopics = {};
let currentTopicsStr = "";

let onTopics = function(topics) {

// check if topics has actually changed, if not, don't do anything
// lazy shortcut to deep compares, might possibly even be faster than
// implementing a deep compare due to
Expand All @@ -117,12 +119,12 @@ let onTopics = function(topics) {
if(newTopicsStr === currentTopicsStr) return;
currentTopics = topics;
currentTopicsStr = newTopicsStr;

let topicTree = treeifyPaths(Object.keys(topics));

$("#topics-nav-ros").empty();
$("#topics-nav-system").empty();

addTopicTreeToNav(topicTree[0], $('#topics-nav-ros'));

$('<a></a>')
Expand Down Expand Up @@ -195,7 +197,7 @@ function initSubscribe({topicName, topicType}) {
subscriptions[topicName] = {
topicType: topicType,
}
}
}
currentTransport.subscribe({topicName: topicName});
if(!subscriptions[topicName].viewer) {
let card = newCard();
Expand All @@ -217,7 +219,6 @@ let currentTransport = null;
function initDefaultTransport() {
currentTransport = new WebSocketV1Transport({
path: "/rosboard/v1",
onOpen: onOpen,
onMsg: onMsg,
onTopics: onTopics,
onSystem: onSystem,
Expand All @@ -236,7 +237,7 @@ function treeifyPaths(paths) {
r[name] = {result: []};
r.result.push({name, children: r[name].result})
}

return r[name];
}, level)
});
Expand Down
25 changes: 15 additions & 10 deletions rosboard/html/js/transports/WebSocketV1Transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ class WebSocketV1Transport {
this.onSystem = onSystem ? onSystem.bind(this) : null;
this.ws = null;
}

connect() {
var protocolPrefix = (window.location.protocol === 'https:') ? 'wss:' : 'ws:';
let abspath = protocolPrefix + '//' + location.host + this.path;

let that = this;

this.ws = new WebSocket(abspath);

this.ws.onopen = function(){
console.log("connected");
if(that.onOpen) that.onOpen(that);
}

this.ws.onclose = function(){
console.log("disconnected");
if(that.onClose) that.onClose(that);
}

this.ws.onmessage = function(wsmsg) {
let data = [];
try {
Expand All @@ -39,7 +39,7 @@ class WebSocketV1Transport {
}

let wsMsgType = data[0];

if(wsMsgType === WebSocketV1Transport.MSG_PING) {
this.send(JSON.stringify([WebSocketV1Transport.MSG_PONG, {
[WebSocketV1Transport.PONG_SEQ]: data[1][WebSocketV1Transport.PING_SEQ],
Expand All @@ -52,27 +52,32 @@ class WebSocketV1Transport {
else console.log("received unknown message: " + wsmsg.data);
}
}

isConnected() {
return (this.ws && this.ws.readyState === this.ws.OPEN);
}

subscribe({topicName, maxUpdateRate = 24.0}) {
this.ws.send(JSON.stringify([WebSocketV1Transport.MSG_SUB, {topicName: topicName, maxUpdateRate: maxUpdateRate}]));
}

unsubscribe({topicName}) {
this.ws.send(JSON.stringify([WebSocketV1Transport.MSG_UNSUB, {topicName: topicName}]));
}

send_plugin_message(name, message) {
this.ws.send(JSON.stringify([WebSocketV1Transport.MSG_PLUGIN_MESSAGE, { name: name, message: message }]));
}
}

WebSocketV1Transport.MSG_PING = "p";
WebSocketV1Transport.MSG_PONG = "q";
WebSocketV1Transport.MSG_MSG = "m";
WebSocketV1Transport.MSG_TOPICS = "t";
WebSocketV1Transport.MSG_SUB = "s";
WebSocketV1Transport.MSG_SYSTEM = "y";
WebSocketV1Transport.MSG_UNSUB = "u";
WebSocketV1Transport.MSG_PLUGIN_MESSAGE = "pm";

WebSocketV1Transport.PING_SEQ= "s";
WebSocketV1Transport.PONG_SEQ = "s";
Expand Down
9 changes: 7 additions & 2 deletions rosboard/html/js/viewers/meta/Viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,14 @@ Viewer.onClose = (viewerInstance) => { console.log("not implemented; override ne
Viewer.onSwitchViewer = (viewerInstance, newViewerType) => { console.log("not implemented; override necessary"); }

// not to be overwritten by child class!
Viewer.registerViewer = (viewer) => {
Viewer.registerViewer = (viewer, toTop=false) => {
// registers a viewer. the viewer child class calls this at the end of the file to register itself
Viewer._viewers.push(viewer);
if (toTop) {
// call with true if it's a plugin
Viewer._viewers.unshift(viewer);
} else {
Viewer._viewers.push(viewer);
}
};

// not to be overwritten by child class!
Expand Down
45 changes: 45 additions & 0 deletions rosboard/plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env python3

import pathlib
import pkgutil
import sys
import traceback


_plugins = {}

def _load_plugins(dirname: str):
global _plugins
for importer, package_name, _ in pkgutil.iter_modules([dirname]):
full_package_name = '%s.%s' % (dirname, package_name)
if full_package_name in sys.modules:
print(f'Plugin "{full_package_name}" not loaded, already present. Check name clashes.')
continue

try:
module = importer.find_module(package_name).load_module(package_name)
_plugins[package_name] = module
except Exception as e:
traceback.print_exception(e)

def get_plugin_js_files():
global _plugins
js_files = []

for plugin in _plugins.values():
js_files.extend(plugin.js_files)

return js_files

def received_plugin_message(name: str, message):
global _plugins
if name not in _plugins:
print(f'Plugin unknown "{name}"')
return

try:
_plugins[name].receive(message)
except Exception as e:
traceback.print_exception(e)

_load_plugins(str(pathlib.Path(__file__).parent.resolve()))
50 changes: 50 additions & 0 deletions rosboard/plugins/plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Example of a possible plugin with ros support:

```python
#!/usr/bin/env python3

import os
import pathlib
import sys

sys.path.append(f"{pathlib.Path(__file__).parent.resolve()}/.")
if os.environ.get("ROS_VERSION") == "1":
import rospy # ROS1
elif os.environ.get("ROS_VERSION") == "2":
import rospy2 as rospy # ROS2
else:
print("ROS not detected. Please source your ROS environment\n(e.g. 'source /opt/ros/DISTRO/setup.bash')")
exit(1)

from std_msgs.msg import String


class DummyPlugin:

def __init__(self):
self.publisher = rospy.Publisher('dummy_topic', String)

def receive(self, message):
msg = String()
msg.data = str(message)
self.publisher.publish(msg)


# This is the most important function for all plugins,
# it's called when a message from the client is sent towards the backend
def receive(message):
global _instance
if _instance == None:
_instance = DummyPlugin()

_instance.receive(message)

_instance = None

# Add all javascript files which are needed for this plugin on the client side:
# e.g. "js/plugins/dummy.js", "js/viewer/dummy.js"
# From client side use the javascript function, like this:
# currentTransport.send_plugin_message("dummy", "myImportantMessage"})
# currentTransport.send_plugin_message("dummy", {myKey: "myImportantMessage"})
js_files = []
```