Skip to content

Commit

Permalink
[py visualization] Add Meldis --meshcat--params option (#20927)
Browse files Browse the repository at this point in the history
  • Loading branch information
jwnimmer-tri authored Feb 13, 2024
1 parent f6aa44f commit ddb32f9
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 8 deletions.
31 changes: 26 additions & 5 deletions bindings/pydrake/visualization/_meldis.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Remove once we have Python >= 3.10.
from __future__ import annotations

import copy
import hashlib
import logging
Expand Down Expand Up @@ -57,6 +60,11 @@

_logger = logging.getLogger("drake")

_DEFAULT_MESHCAT_PARAMS = MeshcatParams(
host="localhost",
show_stats_plot=False,
)


def _to_pose(position, quaternion):
"""Given pose parts, parses it into a RigidTransform.
Expand Down Expand Up @@ -660,8 +668,16 @@ class Meldis:
Refer to the pydrake.visualization.meldis module docs for details.
"""

def __init__(self, *, meshcat_host=None, meshcat_port=None,
environment_map: Path = None):
def __init__(self, *,
meshcat_host: str | None = None,
meshcat_port: int | None = None,
meshcat_params: MeshcatParams | None = None,
environment_map: Path | None = None):
"""Constructs a new Meldis instance. The meshcat_host (when given)
takes precedence over meshcat_params.host. The meshcat_post (when
given) takes precedence over meshcat_params.port.
"""

# Bookkeeping for update throttling.
self._last_update_time = time.time()

Expand All @@ -676,9 +692,14 @@ def __init__(self, *, meshcat_host=None, meshcat_port=None,
lcm_url = self._lcm.get_lcm_url()
_logger.info(f"Meldis is listening for LCM messages at {lcm_url}")

params = MeshcatParams(host=meshcat_host or "localhost",
port=meshcat_port,
show_stats_plot=False)
# Create our meshcat object, merging all of the params goop into one.
if meshcat_params is None:
meshcat_params = _DEFAULT_MESHCAT_PARAMS
params = copy.deepcopy(meshcat_params)
if meshcat_host is not None:
params.host = meshcat_host
if meshcat_port is not None:
params.port = meshcat_port
self.meshcat = Meshcat(params=params)
if environment_map is not None:
self.meshcat.SetEnvironmentMap(environment_map)
Expand Down
24 changes: 21 additions & 3 deletions bindings/pydrake/visualization/meldis.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,14 @@
"""

import argparse
import os
import sys
import webbrowser

from pydrake.common import configure_logging as _configure_logging
from pydrake.common.yaml import yaml_load_typed as _yaml_load_typed
from pydrake.visualization._meldis import Meldis as _Meldis
from pydrake.visualization._meldis import _DEFAULT_MESHCAT_PARAMS


def _available_browsers():
Expand All @@ -43,7 +47,10 @@ def _available_browsers():
return []


def _main():
def _main(args=None):
# Make cwd be what the user expected, not the runfiles tree.
if "BUILD_WORKING_DIRECTORY" in os.environ:
os.chdir(os.environ["BUILD_WORKING_DIRECTORY"])
_configure_logging()
parser = argparse.ArgumentParser()
parser.add_argument(
Expand Down Expand Up @@ -74,14 +81,25 @@ def _main():
"--idle-timeout", metavar="TIME", type=float, default=15*60,
help="When no web browser has been connected for this many seconds,"
" this program will automatically exit. Set to 0 to run indefinitely.")
parser.add_argument(
"--meshcat-params", metavar="PATH",
help="Filesystem path to a YAML or JSON config for MeshcatParams. "
"This can be used to configure Meshcat's initial properties. "
"For options that are available as both command line arguments and "
"YAML params (e.g., --port), the command line takes precedence.")
parser.add_argument(
"--environment_map", metavar="PATH",
help="Filesystem path to an image to be used as an environment map. "
"It must be an image type normally used by your browser (e.g., "
".jpg, .png, etc.). HDR images are not supported yet."
)
args = parser.parse_args()
args = parser.parse_args(args)
meshcat_params = None
if args.meshcat_params is not None:
meshcat_params = _yaml_load_typed(
filename=args.meshcat_params, defaults=_DEFAULT_MESHCAT_PARAMS)
meldis = _Meldis(meshcat_host=args.host, meshcat_port=args.port,
meshcat_params=meshcat_params,
environment_map=args.environment_map)
if args.browser is not None and args.browser_new is None:
args.browser_new = 1
Expand All @@ -101,4 +119,4 @@ def _main():


if __name__ == "__main__":
_main()
_main(args=sys.argv[1:])
28 changes: 28 additions & 0 deletions bindings/pydrake/visualization/test/meldis_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import os
from pathlib import Path
import sys
import tempfile
import unittest

import numpy as np
Expand All @@ -33,6 +34,7 @@
from pydrake.geometry import (
DrakeVisualizer,
DrakeVisualizerParams,
MeshcatParams,
Role,
)
from pydrake.lcm import (
Expand Down Expand Up @@ -626,9 +628,35 @@ def test_draw_frame_applet(self):
# After the handlers are called, we have the expected meshcat path.
self.assertEqual(dut.meshcat.HasPath(meshcat_path), True)

def test_args_precedence(self):
"""Checks that the "kwargs wins" part of our API contract is met.
"""
# When bad MeshcatParams are used Meldis rejects them, but good kwargs
# can override them and win (no errors).
bad_host = MeshcatParams(host="8.8.8.8")
bad_port = MeshcatParams(port=1)
with self.assertRaises(BaseException):
mut.Meldis(meshcat_params=bad_host)
with self.assertRaises(BaseException):
mut.Meldis(meshcat_params=bad_port)
mut.Meldis(meshcat_params=bad_host, meshcat_host="localhost")
mut.Meldis(meshcat_params=bad_port, meshcat_port=0)

def test_command_line_browser_names(self):
"""Sanity checks our webbrowser names logic. The objective is to return
some kind of a list, without crashing.
"""
names = pydrake.visualization.meldis._available_browsers()
self.assertIsInstance(names, list)

def test_command_meshcat_params(self):
"""Confirm that the params plumbing works by feeding in bad params and
seeing a validation error be spit out.
"""
main = pydrake.visualization.meldis._main
with tempfile.TemporaryDirectory() as temp:
path = Path(temp) / "meshcat_params.yaml"
path.write_text("{ port: 1 }", encoding="utf-8")
args = [f"--meshcat-params={path}"]
with self.assertRaisesRegex(BaseException, "port.*>.*1024"):
main(args=args)

0 comments on commit ddb32f9

Please sign in to comment.