From b813c56e5d95c8478b931a0fde7bc99ba1187ff8 Mon Sep 17 00:00:00 2001 From: Nabla7 Date: Tue, 13 Jan 2026 13:36:23 +0100 Subject: [PATCH 1/2] feat(cli): type-free topic echo via /topic#pkg.Msg inference, this mirrors ros topic echo functionality. - Make type_name optional in 'dimos topic echo' - Infer message type from LCM channel suffix (e.g. /odom#nav_msgs.Odometry) - Dynamically import dimos.msgs. and call cls.lcm_decode(data) - Keep existing explicit-type mode working - Update transports.md docs --- dimos/robot/cli/dimos.py | 5 +++- dimos/robot/cli/topic.py | 55 +++++++++++++++++++++++++++++-------- docs/concepts/transports.md | 6 ++++ 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/dimos/robot/cli/dimos.py b/dimos/robot/cli/dimos.py index 5cf09e02e3..a000502abc 100644 --- a/dimos/robot/cli/dimos.py +++ b/dimos/robot/cli/dimos.py @@ -184,7 +184,10 @@ def humancli(ctx: typer.Context) -> None: @topic_app.command() def echo( topic: str = typer.Argument(..., help="Topic name to listen on (e.g., /goal_request)"), - type_name: str = typer.Argument(..., help="Message type (e.g., PoseStamped)"), + type_name: str | None = typer.Argument( + None, + help="Optional message type (e.g., PoseStamped). If omitted, infer from '/topic#pkg.Msg'.", + ), ) -> None: topic_echo(topic, type_name) diff --git a/dimos/robot/cli/topic.py b/dimos/robot/cli/topic.py index bdd1a29ae6..92b6e8cc3b 100644 --- a/dimos/robot/cli/topic.py +++ b/dimos/robot/cli/topic.py @@ -12,12 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations + import importlib +import re import time +import lcm # type: ignore[import-untyped] import typer from dimos.core.transport import LCMTransport, pLCMTransport +from dimos.protocol.service.lcmservice import autoconf _modules_to_try = [ "dimos.msgs.geometry_msgs", @@ -42,23 +47,49 @@ def _resolve_type(type_name: str) -> type: raise ValueError(f"Could not find type '{type_name}' in any known message modules") -def topic_echo(topic: str, type_name: str) -> None: - msg_type = _resolve_type(type_name) - use_pickled = getattr(msg_type, "lcm_encode", None) is None - transport: pLCMTransport[object] | LCMTransport[object] = ( - pLCMTransport(topic) if use_pickled else LCMTransport(topic, msg_type) - ) - - def _on_message(msg: object) -> None: - print(msg) +def topic_echo(topic: str, type_name: str | None) -> None: + # Explicit mode (legacy): unchanged. + if type_name is not None: + msg_type = _resolve_type(type_name) + use_pickled = getattr(msg_type, "lcm_encode", None) is None + transport: pLCMTransport[object] | LCMTransport[object] = ( + pLCMTransport(topic) if use_pickled else LCMTransport(topic, msg_type) + ) - transport.subscribe(_on_message) + def _on_message(msg: object) -> None: + print(msg) - typer.echo(f"Listening on {topic} for {type_name} messages... (Ctrl+C to stop)") + transport.subscribe(_on_message) + typer.echo(f"Listening on {topic} for {type_name} messages... (Ctrl+C to stop)") + try: + while True: + time.sleep(0.1) + except KeyboardInterrupt: + typer.echo("\nStopped.") + return + + # Inferred typed mode: listen on /topic#pkg.Msg and decode from the msg_name suffix. + autoconf() + l = lcm.LCM() + typed_pattern = rf"^{re.escape(topic)}#.*" + + def on_msg(channel: str, data: bytes) -> None: + _, msg_name = channel.split("#", 1) # e.g. "nav_msgs.Odometry" + pkg, cls_name = msg_name.split(".", 1) # "nav_msgs", "Odometry" + module = importlib.import_module(f"dimos.msgs.{pkg}") + cls = getattr(module, cls_name) + print(cls.lcm_decode(data)) + + l.subscribe(typed_pattern, on_msg) + + typer.echo( + f"Listening on {topic} (inferring from typed LCM channels like '{topic}#pkg.Msg')... " + "(Ctrl+C to stop)" + ) try: while True: - time.sleep(0.1) + l.handle_timeout(50) except KeyboardInterrupt: typer.echo("\nStopped.") diff --git a/docs/concepts/transports.md b/docs/concepts/transports.md index fe06334fe9..b0257114ac 100644 --- a/docs/concepts/transports.md +++ b/docs/concepts/transports.md @@ -132,6 +132,12 @@ lcm.stop() Received velocity: x=1.0, y=0.0, z=0.5 ``` +### Inspecting LCM traffic (CLI) + +- `dimos lcmspy` shows topic frequency/bandwidth stats. +- `dimos topic echo /topic` listens on typed channels like `/topic#pkg.Msg` and decodes automatically. +- `dimos topic echo /topic TypeName` is the explicit legacy form. + ## Encoder Mixins Transports can use encoder mixins to serialize messages. The `PubSubEncoderMixin` pattern wraps publish/subscribe to encode/decode automatically: From e906caf653f2a1d61e72d95ed4406476246a7ed3 Mon Sep 17 00:00:00 2001 From: Nabla7 Date: Tue, 13 Jan 2026 13:46:52 +0100 Subject: [PATCH 2/2] fix(cli): use LCMPubSubBase instead of raw lcm.LCM for topic echo, my bad --- dimos/robot/cli/topic.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/dimos/robot/cli/topic.py b/dimos/robot/cli/topic.py index 92b6e8cc3b..582099c4b6 100644 --- a/dimos/robot/cli/topic.py +++ b/dimos/robot/cli/topic.py @@ -18,11 +18,10 @@ import re import time -import lcm # type: ignore[import-untyped] import typer from dimos.core.transport import LCMTransport, pLCMTransport -from dimos.protocol.service.lcmservice import autoconf +from dimos.protocol.pubsub.lcmpubsub import LCMPubSubBase _modules_to_try = [ "dimos.msgs.geometry_msgs", @@ -69,8 +68,9 @@ def _on_message(msg: object) -> None: return # Inferred typed mode: listen on /topic#pkg.Msg and decode from the msg_name suffix. - autoconf() - l = lcm.LCM() + bus = LCMPubSubBase(autoconf=True) + bus.start() # starts threaded handle loop + typed_pattern = rf"^{re.escape(topic)}#.*" def on_msg(channel: str, data: bytes) -> None: @@ -80,7 +80,8 @@ def on_msg(channel: str, data: bytes) -> None: cls = getattr(module, cls_name) print(cls.lcm_decode(data)) - l.subscribe(typed_pattern, on_msg) + assert bus.l is not None + bus.l.subscribe(typed_pattern, on_msg) typer.echo( f"Listening on {topic} (inferring from typed LCM channels like '{topic}#pkg.Msg')... " @@ -89,8 +90,9 @@ def on_msg(channel: str, data: bytes) -> None: try: while True: - l.handle_timeout(50) + time.sleep(0.1) except KeyboardInterrupt: + bus.stop() typer.echo("\nStopped.")