Skip to content

Commit

Permalink
[geometry] Establishes Meshcat in C++ (step 1)
Browse files Browse the repository at this point in the history
This is the first of a series of PRs that will provide Meshcat as a visualizer in C++.  The design and PR strategy is documented in RobotLocomotion#13038.

This is the first PR:
Meshcat proof of life. Starts the server, demonstrates that clients can connect, and just sends one type of message to show that data can flow. Reviewers can focus on the build system and websocket server details.

The dependencies added here are all quite lightweight, and properly licensed.  And at the end of the PR train, we will likely want to deprecate meshcat-python, and eventually remove the pip dependencies that come along with it.
  • Loading branch information
RussTedrake committed Aug 19, 2021
1 parent b026886 commit f4a6f95
Show file tree
Hide file tree
Showing 21 changed files with 449 additions and 1 deletion.
1 change: 1 addition & 0 deletions doc/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ filegroup(
# https://developer.mozilla.org/en-US/docs/Web/Manifest
"site.webmanifest",
],
visibility = ["//visibility:public"],
)

filegroup(
Expand Down
36 changes: 36 additions & 0 deletions geometry/dev/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# -*- python -*-

load(
"@drake//tools/skylark:drake_cc.bzl",
"drake_cc_binary",
"drake_cc_library",
)
load("//tools/lint:lint.bzl", "add_lint_tests")

package(default_visibility = ["//visibility:private"])

drake_cc_library(
name = "meshcat",
srcs = ["meshcat.cc"],
hdrs = ["meshcat.h"],
data = [
"//doc:favicon",
"@meshcat//:dist/index.html",
"@meshcat//:dist/main.min.js",
],
deps = [
"//common:essential",
"//common:find_resource",
"//common:unused",
"@msgpack",
"@uwebsockets",
],
)

drake_cc_binary(
name = "meshcat_demo",
srcs = ["meshcat_demo.cc"],
deps = ["meshcat"],
)

add_lint_tests()
178 changes: 178 additions & 0 deletions geometry/dev/meshcat.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
#include "drake/geometry/dev/meshcat.h"

#include <fstream>
#include <future>
#include <sstream>
#include <string>
#include <thread>
#include <unordered_map>
#include <utility>

#include <App.h>
#include <msgpack.hpp>

#include "drake/common/find_resource.h"
#include "drake/common/never_destroyed.h"
#include "drake/common/unused.h"

namespace {
std::string LoadFile(const std::string& filename) {
const std::string resource = drake::FindResourceOrThrow(filename);
std::ifstream file(resource.c_str(), std::ios::in);
if (!file.is_open())
throw std::runtime_error("Error opening file: " + filename);
std::stringstream content;
content << file.rdbuf();
file.close();
return content.str();
}
} // namespace

namespace drake {
namespace geometry {

namespace {

constexpr static bool SSL = false;
struct PerSocketData {
// Intentionally left empty.
};
using WebSocket = uWS::WebSocket<SSL, true, PerSocketData>;

} // namespace

class Meshcat::WebSocketPublisher {
public:
DRAKE_NO_COPY_NO_MOVE_NO_ASSIGN(WebSocketPublisher);

WebSocketPublisher() : app_future(app_promise.get_future()) {}

// Call this from websocket thread.
void SetAppPromise(uWS::App* app, uWS::Loop* loop) {
app_promise.set_value(std::make_pair(app, loop));
}

// Call this from main thread.
void GetAppFuture() {
std::tie(app_, loop_) = app_future.get();
}

template <typename T>
void SetProperty(const std::string& path, const std::string& property,
const T& value) {
DRAKE_ASSERT(app_ != nullptr);
DRAKE_ASSERT(loop_ != nullptr);

std::stringstream message;

msgpack::zone z;
msgpack::pack(message, std::unordered_map<std::string, msgpack::object>(
{{"type", msgpack::object("set_property", z)},
{"path", msgpack::object(path, z)},
{"property", msgpack::object(property, z)},
{"value", msgpack::object(value, z)}}));

// Note: Must pass path and property by value because they will go out of
// scope.
loop_->defer([this, path, property, msg = message.str()]() {
app_->publish("all", msg, uWS::OpCode::BINARY, false);
set_properties_[path + "/" + property] = msg;
});
}

void SendTree(WebSocket* ws) {
// TODO(russt): Generalize this to publishing the entire scene tree.
for (const auto& [key, msg] : set_properties_) {
unused(key);
ws->send(msg);
}
}

private:
std::promise<std::pair<uWS::App*, uWS::Loop*>> app_promise{};
std::future<std::pair<uWS::App*, uWS::Loop*>> app_future{};

// Only loop_->defer() should be called from outside the websocket_thread. See
// the documentation for uWebSockets for further details:
// https://github.com/uNetworking/uWebSockets/blob/d94bf2cd43bed5e0de396a8412f156e15c141e98/misc/READMORE.md#threading
uWS::Loop* loop_{nullptr};

// The remaining variables should only be accessed from the websocket_thread.
uWS::App* app_{nullptr};
std::unordered_map<std::string, std::string> set_properties_{};
};

Meshcat::Meshcat() {
// A std::promise is made in the WebSocketPublisher.
publisher_ = std::make_unique<WebSocketPublisher>();
websocket_thread_ = std::thread(&Meshcat::WebsocketMain, this);
// The std::promise is full-filled in WebsocketMain; we wait here to obtain
// that value.
publisher_->GetAppFuture();
}

Meshcat::~Meshcat() = default;

void Meshcat::JoinWebSocketThread() { websocket_thread_.join(); }

void Meshcat::SetProperty(const std::string& path, const std::string& property,
bool value) {
publisher_->SetProperty(path, property, value);
}

void Meshcat::WebsocketMain() {
// Preload the three files we will serve (always).
static const drake::never_destroyed<std::string> index_html(
LoadFile("drake/external/meshcat/dist/index.html"));
static const drake::never_destroyed<std::string> main_min_js(
LoadFile("drake/external/meshcat/dist/main.min.js"));
static const drake::never_destroyed<std::string> favicon_ico(
LoadFile("drake/doc/favicon.ico"));
int port = 7001;
const int kMaxPort = 7099;

uWS::App::WebSocketBehavior<PerSocketData> behavior;
behavior.open = [this](WebSocket* ws) {
ws->subscribe("all");
// Update this new connection with previously published data.
publisher_->SendTree(ws);
};

uWS::App app =
uWS::App()
.get("/*",
[&](uWS::HttpResponse<SSL>* res, uWS::HttpRequest* req) {
if (req->getUrl() == "/main.min.js") {
res->end(main_min_js.access());
} else if (req->getUrl() == "/favicon.ico") {
res->end(favicon_ico.access());
} else {
res->end(index_html.access());
}
})
.ws<PerSocketData>("/*", std::move(behavior));

bool listening = false;
do {
app.listen(
port, LIBUS_LISTEN_EXCLUSIVE_PORT,
[port, &listening](us_listen_socket_t* listenSocket) {
if (listenSocket) {
std::cout
<< "Meshcat listening for connections at http://127.0.0.1:"
<< port << std::endl;
listening = true;
}
});
} while (!listening && port++ <= kMaxPort);

publisher_->SetAppPromise(&app, uWS::Loop::get());

app.run();

// run() should not terminate. If it does, then we've failed.
throw std::runtime_error("Meshcat websocket thread failed");
}

} // namespace geometry
} // namespace drake
57 changes: 57 additions & 0 deletions geometry/dev/meshcat.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#pragma once

#include <memory>
#include <string>
#include <thread>

#include "drake/common/drake_copyable.h"

namespace drake {
namespace geometry {

/** Provides an interface to https://github.com/rdeits/meshcat.
Each instance of this class spawns a thread which runs an http/websocket server.
Users can navigate their browser to the hosted URL to visualize the Meshcat
scene. Note that, unlike many visualizers, one cannot open the visualizer until
this server is running.
Note: This code is currently a skeleton implementation; it only allows you to
set (boolean) properties as a minimal demonstration of sending data from C++ to
the viewer. It is the result of the first PR in a train of PRs that will
establish the full Meshcat functionality. See #13038.
*/
class Meshcat {
public:
DRAKE_NO_COPY_NO_MOVE_NO_ASSIGN(Meshcat)

/** Constructs the Meshcat instance. It will listen on the first available
port starting at 7001 (up to 7099). */
Meshcat();

~Meshcat();

/** The thread run by this class will run for the lifetime of the Meshcat
instance. We provide this method as a simple way to block the main thread
to allow users to open their browser and work with the Meshcat scene. */
void JoinWebSocketThread();

/** Forwards a set_property(...) message to the meshcat viewers. For example,
@verbatim
meshcat.SetProperty("/Background", "visible", false);
@endverbatim
will turn off the background. */
void SetProperty(const std::string& path, const std::string& property,
bool value);

private:
void WebsocketMain();
std::thread websocket_thread_{};

// Provides PIMPL encapsulation of websocket types.
class WebSocketPublisher;
std::unique_ptr<WebSocketPublisher> publisher_;
};

} // namespace geometry
} // namespace drake
32 changes: 32 additions & 0 deletions geometry/dev/meshcat_demo.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include "drake/geometry/dev/meshcat.h"

/** This binary provides a simple demonstration of using Meshcat. It serves as
a stand-in for the proper test suite that will come in the next PR, since it
requires it's own substantial changes to the build system.
To test, you must manually run `bazel run //geometry/dev:meshcat_demo`. It will
print two URLs to console. Navigating your browser to the first, you should see
that the normally blue meshcat background is not visible (the background will
look white). In the second URL, you should see the default meshcat view, but
the grid that normally shows the ground plane is not visible.
*/

int main() {
drake::geometry::Meshcat meshcat;

// Note: this will only send one message to any new server.
meshcat.SetProperty("/Background", "visible", false);
meshcat.SetProperty("/Background", "visible", true);
meshcat.SetProperty("/Background", "visible", false);

// Demonstrate that we can construct multiple meshcats (and they will serve on
// different ports).
drake::geometry::Meshcat meshcat2;
meshcat2.SetProperty("/Grid", "visible", false);

// Effectively sleep forever.
meshcat.JoinWebSocketThread();
meshcat2.JoinWebSocketThread();

return 0;
}
1 change: 1 addition & 0 deletions setup/mac/binary_distribution/Brewfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ brew 'libyaml'
brew 'lz4'
brew 'nlopt'
brew 'numpy'
brew 'msgpack'
brew 'openblas'
brew 'pkg-config'
brew 'python@3.9'
Expand Down
1 change: 1 addition & 0 deletions setup/ubuntu/binary_distribution/packages-bionic.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ liblapack3
libldl2
liblz4-1
liblzma5
libmsgpackc2
libmumps-seq-5.1.2
libnetcdf13
libnlopt0
Expand Down
1 change: 1 addition & 0 deletions setup/ubuntu/binary_distribution/packages-focal.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ liblapack3
libldl2
liblz4-1
liblzma5
libmsgpackc2
libmumps-seq-5.2.1
libnetcdf15
libnlopt-cxx0
Expand Down
1 change: 1 addition & 0 deletions setup/ubuntu/source_distribution/packages-bionic.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ libjsoncpp-dev
liblapack-dev
liblz4-dev
liblzma-dev
libmsgpack-dev
libmumps-seq-dev
libnlopt-dev
libpng-dev
Expand Down
1 change: 1 addition & 0 deletions setup/ubuntu/source_distribution/packages-focal.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ libjsoncpp-dev
liblapack-dev
liblz4-dev
liblzma-dev
libmsgpack-dev
libmumps-seq-dev
libnlopt-cxx-dev
libopengl-dev
Expand Down
2 changes: 2 additions & 0 deletions tools/workspace/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ _DRAKE_EXTERNAL_PACKAGE_INSTALLS = ["@%s//:install" % p for p in [
"sdformat",
"spdlog",
"tinyobjloader",
"usockets",
"uwebsockets",
"vtk",
]] + ["//tools/workspace/%s:install" % p for p in [
"cds",
Expand Down
Loading

0 comments on commit f4a6f95

Please sign in to comment.