forked from RobotLocomotion/drake
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[geometry] Establishes Meshcat in C++ (step 1)
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
1 parent
b026886
commit 37fd417
Showing
21 changed files
with
447 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ brew 'libyaml' | |
brew 'lz4' | ||
brew 'nlopt' | ||
brew 'numpy' | ||
brew 'msgpack' | ||
brew 'openblas' | ||
brew 'pkg-config' | ||
brew '[email protected]' | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ liblapack3 | |
libldl2 | ||
liblz4-1 | ||
liblzma5 | ||
libmsgpackc2 | ||
libmumps-seq-5.1.2 | ||
libnetcdf13 | ||
libnlopt0 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,7 @@ liblapack3 | |
libldl2 | ||
liblz4-1 | ||
liblzma5 | ||
libmsgpackc2 | ||
libmumps-seq-5.2.1 | ||
libnetcdf15 | ||
libnlopt-cxx0 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.