From aeab9d96b80384813ce8b3f07670006159edf02c Mon Sep 17 00:00:00 2001 From: Paul Nechifor Date: Tue, 7 Jul 2026 10:09:18 +0300 Subject: [PATCH] fix: be able to send messages --- dimos/robot/cli/test_topic.py | 50 ++++++++++++++++++++++++++++++++++- dimos/robot/cli/topic.py | 42 ++++++++++++++--------------- 2 files changed, 70 insertions(+), 22 deletions(-) diff --git a/dimos/robot/cli/test_topic.py b/dimos/robot/cli/test_topic.py index add79603c1..c2245cd64f 100644 --- a/dimos/robot/cli/test_topic.py +++ b/dimos/robot/cli/test_topic.py @@ -12,8 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest + +from dimos.core.global_config import global_config +from dimos.core.transport import PubSubTransport +from dimos.core.transport_factory import make_transport +from dimos.msgs.geometry_msgs.Twist import Twist +from dimos.msgs.geometry_msgs.Vector3 import Vector3 from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo -from dimos.robot.cli.topic import _decode_typed_lcm_message +from dimos.protocol.pubsub.impl.lcmpubsub import LCM, Topic +from dimos.robot.cli.topic import _build_eval_context, _decode_typed_lcm_message, topic_send +from dimos.utils.testing.collector import CallbackCollector def test_decode_typed_lcm_message_resolves_message_submodule() -> None: @@ -34,3 +43,42 @@ def test_decode_typed_lcm_message_resolves_message_submodule() -> None: assert decoded.height == 1080 assert decoded.frame_id == "camera_optical" assert decoded.distortion_model == "plumb_bob" + + +def test_build_eval_context_maps_message_names_to_classes() -> None: + context = _build_eval_context() + + for name in ("Twist", "Vector3", "PoseStamped"): + cls = context[name] + assert isinstance(cls, type) + assert cls.__name__ == name + + +def test_topic_send_delivers_over_lcm(monkeypatch: pytest.MonkeyPatch, lcm_url: str) -> None: + monkeypatch.setattr(global_config, "transport", "lcm") + + # topic_send never stops the transport it creates; capture it so its LCM + # threads can be stopped (the thread-leak check fails the test otherwise). + transports: list[PubSubTransport[object]] = [] + + def capturing_make_transport(name: str, msg_type: type) -> PubSubTransport[object]: + transport: PubSubTransport[object] = make_transport(name, msg_type) + transports.append(transport) + return transport + + monkeypatch.setattr("dimos.robot.cli.topic.make_transport", capturing_make_transport) + + bus = LCM(url=lcm_url) + bus.start() + collector = CallbackCollector(1) + bus.subscribe(Topic(topic="/test_topic_send", lcm_type=Twist), collector) + + try: + topic_send("/test_topic_send", "Twist(Vector3(0.5, 0, 0), Vector3(0, 0, 0))") + collector.wait() + finally: + for transport in transports: + transport.stop() + bus.stop() + + assert collector.results[0][0] == Twist(Vector3(0.5, 0, 0), Vector3(0, 0, 0)) diff --git a/dimos/robot/cli/topic.py b/dimos/robot/cli/topic.py index 4e793eb472..1e5dd05dbd 100644 --- a/dimos/robot/cli/topic.py +++ b/dimos/robot/cli/topic.py @@ -16,6 +16,7 @@ from collections.abc import Callable import importlib +import pkgutil import re import time @@ -130,30 +131,29 @@ def _topic_echo_inferred_zenoh(topic: str) -> None: ) -def topic_send(topic: str, message_expr: str) -> None: +def _build_eval_context() -> dict[str, object]: + # The msgs packages are namespace packages (no __init__.py), so walk their + # submodules; each message file defines a class of the same name. eval_context: dict[str, object] = {} - modules_to_import = [ - "dimos.msgs.geometry_msgs", - "dimos.msgs.nav_msgs", - "dimos.msgs.sensor_msgs", - "dimos.msgs.std_msgs", - "dimos.msgs.vision_msgs", - "dimos.msgs.tf2_msgs", - ] - - for module_name in modules_to_import: - try: - module = importlib.import_module(module_name) - for name in dir(module): - if not name.startswith("_"): - obj = getattr(module, name, None) - if obj is not None: - eval_context[name] = obj - except ImportError: - continue + for package_name in _modules_to_try: + package = importlib.import_module(package_name) + for module_info in pkgutil.iter_modules(package.__path__): + name = module_info.name + if name.startswith("test_"): + continue + try: + submodule = importlib.import_module(f"{package_name}.{name}") + except ImportError: + continue + obj = getattr(submodule, name, None) + if obj is not None: + eval_context[name] = obj + return eval_context + +def topic_send(topic: str, message_expr: str) -> None: try: - message = eval(message_expr, eval_context) + message = eval(message_expr, _build_eval_context()) except Exception as e: typer.echo(f"Error parsing message: {e}", err=True) raise typer.Exit(1)