diff --git a/.gitignore b/.gitignore index f6eaac7f5b..6378d36468 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,12 @@ -.venv/ .vscode/ # Ignore Python cache files __pycache__/ *.pyc -.venv* -venv* + +# Ignore virtual environment directories +*venv*/ +.venv*/ .ssh/ # Ignore python tooling dirs diff --git a/dimos/msgs/geometry_msgs/Pose.py b/dimos/msgs/geometry_msgs/Pose.py index 33f0ae22a9..eb1e879709 100644 --- a/dimos/msgs/geometry_msgs/Pose.py +++ b/dimos/msgs/geometry_msgs/Pose.py @@ -19,7 +19,7 @@ from io import BytesIO from typing import BinaryIO, TypeAlias -from lcm_msgs.geometry_msgs import Pose as LCMPose +from dimos_lcm.geometry_msgs import Pose as LCMPose from plum import dispatch from dimos.msgs.geometry_msgs.Quaternion import Quaternion, QuaternionConvertable diff --git a/dimos/msgs/geometry_msgs/PoseStamped.py b/dimos/msgs/geometry_msgs/PoseStamped.py index 2a35ccf445..237cf31225 100644 --- a/dimos/msgs/geometry_msgs/PoseStamped.py +++ b/dimos/msgs/geometry_msgs/PoseStamped.py @@ -17,9 +17,9 @@ from io import BytesIO from typing import BinaryIO, TypeAlias -from lcm_msgs.geometry_msgs import PoseStamped as LCMPoseStamped -from lcm_msgs.std_msgs import Header as LCMHeader -from lcm_msgs.std_msgs import Time as LCMTime +from dimos_lcm.geometry_msgs import PoseStamped as LCMPoseStamped +from dimos_lcm.std_msgs import Header as LCMHeader +from dimos_lcm.std_msgs import Time as LCMTime from plum import dispatch from dimos.msgs.geometry_msgs.Pose import Pose diff --git a/dimos/msgs/geometry_msgs/Quaternion.py b/dimos/msgs/geometry_msgs/Quaternion.py index dfb0e21d95..3fb13df532 100644 --- a/dimos/msgs/geometry_msgs/Quaternion.py +++ b/dimos/msgs/geometry_msgs/Quaternion.py @@ -20,7 +20,7 @@ from typing import BinaryIO, TypeAlias import numpy as np -from lcm_msgs.geometry_msgs import Quaternion as LCMQuaternion +from dimos_lcm.geometry_msgs import Quaternion as LCMQuaternion from plum import dispatch from dimos.msgs.geometry_msgs.Vector3 import Vector3 diff --git a/dimos/msgs/geometry_msgs/Vector3.py b/dimos/msgs/geometry_msgs/Vector3.py index 0d63300505..896d6bc43b 100644 --- a/dimos/msgs/geometry_msgs/Vector3.py +++ b/dimos/msgs/geometry_msgs/Vector3.py @@ -20,7 +20,7 @@ from typing import BinaryIO, TypeAlias import numpy as np -from lcm_msgs.geometry_msgs import Vector3 as LCMVector3 +from dimos_lcm.geometry_msgs import Vector3 as LCMVector3 from plum import dispatch # Types that can be converted to/from Vector diff --git a/dimos/msgs/geometry_msgs/test_Pose.py b/dimos/msgs/geometry_msgs/test_Pose.py index 9dc5330f7f..590a17549c 100644 --- a/dimos/msgs/geometry_msgs/test_Pose.py +++ b/dimos/msgs/geometry_msgs/test_Pose.py @@ -16,7 +16,7 @@ import numpy as np import pytest -from lcm_msgs.geometry_msgs import Pose as LCMPose +from dimos_lcm.geometry_msgs import Pose as LCMPose from dimos.msgs.geometry_msgs.Pose import Pose, to_pose from dimos.msgs.geometry_msgs.Quaternion import Quaternion diff --git a/dimos/msgs/geometry_msgs/test_Quaternion.py b/dimos/msgs/geometry_msgs/test_Quaternion.py index 7f20143e2c..ab049f809f 100644 --- a/dimos/msgs/geometry_msgs/test_Quaternion.py +++ b/dimos/msgs/geometry_msgs/test_Quaternion.py @@ -14,7 +14,7 @@ import numpy as np import pytest -from lcm_msgs.geometry_msgs import Quaternion as LCMQuaternion +from dimos_lcm.geometry_msgs import Quaternion as LCMQuaternion from dimos.msgs.geometry_msgs.Quaternion import Quaternion diff --git a/dimos/msgs/sensor_msgs/Image.py b/dimos/msgs/sensor_msgs/Image.py index e32a838dfc..6179746340 100644 --- a/dimos/msgs/sensor_msgs/Image.py +++ b/dimos/msgs/sensor_msgs/Image.py @@ -21,8 +21,8 @@ import numpy as np # Import LCM types -from lcm_msgs.sensor_msgs.Image import Image as LCMImage -from lcm_msgs.std_msgs.Header import Header +from dimos_lcm.sensor_msgs.Image import Image as LCMImage +from dimos_lcm.std_msgs.Header import Header from dimos.types.timestamped import Timestamped diff --git a/dimos/msgs/sensor_msgs/PointCloud2.py b/dimos/msgs/sensor_msgs/PointCloud2.py index b2835196ea..776c81d056 100644 --- a/dimos/msgs/sensor_msgs/PointCloud2.py +++ b/dimos/msgs/sensor_msgs/PointCloud2.py @@ -22,9 +22,11 @@ import open3d as o3d # Import LCM types -from lcm_msgs.sensor_msgs.PointCloud2 import PointCloud2 as LCMPointCloud2 -from lcm_msgs.sensor_msgs.PointField import PointField -from lcm_msgs.std_msgs.Header import Header +from dimos_lcm.sensor_msgs.PointCloud2 import ( + PointCloud2 as LCMPointCloud2, +) +from dimos_lcm.sensor_msgs.PointField import PointField +from dimos_lcm.std_msgs.Header import Header from dimos.types.timestamped import Timestamped diff --git a/dimos/robot/foxglove_bridge.py b/dimos/robot/foxglove_bridge.py new file mode 100644 index 0000000000..a0374fc251 --- /dev/null +++ b/dimos/robot/foxglove_bridge.py @@ -0,0 +1,49 @@ +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio +import threading + +# this is missing, I'm just trying to import lcm_foxglove_bridge.py from dimos_lcm +import dimos_lcm.lcm_foxglove_bridge as bridge + +from dimos.core import Module, rpc + + +class FoxgloveBridge(Module): + _thread: threading.Thread + _loop: asyncio.AbstractEventLoop + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.start() + + @rpc + def start(self): + def run_bridge(): + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + try: + self._loop.run_until_complete(bridge.main()) + except Exception as e: + print(f"Foxglove bridge error: {e}") + + self._thread = threading.Thread(target=run_bridge, daemon=True) + self._thread.start() + + @rpc + def stop(self): + if self._loop and self._loop.is_running(): + self._loop.call_soon_threadsafe(self._loop.stop) + self._thread.join(timeout=2) diff --git a/dimos/robot/unitree_webrtc/multiprocess/unitree_go2.py b/dimos/robot/unitree_webrtc/multiprocess/unitree_go2.py index 659eafde0c..1740b7edbc 100644 --- a/dimos/robot/unitree_webrtc/multiprocess/unitree_go2.py +++ b/dimos/robot/unitree_webrtc/multiprocess/unitree_go2.py @@ -30,6 +30,7 @@ from dimos.msgs.geometry_msgs import Vector3 from dimos.msgs.sensor_msgs import Image from dimos.protocol import pubsub +from dimos.robot.foxglove_bridge import FoxgloveBridge from dimos.robot.global_planner import AstarPlanner from dimos.robot.local_planner.simple import SimplePlanner from dimos.robot.unitree_webrtc.connection import VideoMessage, WebRTCRobot @@ -182,6 +183,8 @@ async def run(ip): ctrl.plancmd.transport = core.LCMTransport("/global_target", Vector3) global_planner.target.connect(ctrl.plancmd) + foxglove_bridge = FoxgloveBridge() + # we review the structure print("\n") for module in [connection, mapper, local_planner, global_planner, ctrl]: @@ -199,6 +202,9 @@ async def run(ip): print(colors.green("starting global planner")) global_planner.start() + print(colors.green("starting foxglove bridge")) + foxglove_bridge.start() + # uncomment to move the bot # print(colors.green("starting ctrl")) # ctrl.start() diff --git a/dimos/utils/run_foxglove_bridge.py b/dimos/utils/run_foxglove_bridge.py new file mode 100644 index 0000000000..dadb7c2529 --- /dev/null +++ b/dimos/utils/run_foxglove_bridge.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +use lcm_foxglove_bridge as a module from dimos_lcm +""" + +import asyncio +import threading +import dimos_lcm.lcm_foxglove_bridge as bridge + + +def run_bridge_example(): + """Example of running the bridge in a separate thread""" + + def bridge_thread(): + """Thread function to run the bridge""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + bridge_runner = bridge.LcmFoxgloveBridgeRunner( + host="0.0.0.0", port=8765, debug=True, num_threads=4 + ) + + loop.run_until_complete(bridge_runner.run()) + except Exception as e: + print(f"Bridge error: {e}") + finally: + loop.close() + + thread = threading.Thread(target=bridge_thread, daemon=True) + thread.start() + + print("Bridge started in background thread") + print("Open Foxglove Studio and connect to ws://localhost:8765") + print("Press Ctrl+C to exit") + + try: + while True: + threading.Event().wait(1) + except KeyboardInterrupt: + print("Shutting down...") + + +if __name__ == "__main__": + run_bridge_example() diff --git a/dimos/utils/test_foxglove_bridge.py b/dimos/utils/test_foxglove_bridge.py new file mode 100644 index 0000000000..ecedb90573 --- /dev/null +++ b/dimos/utils/test_foxglove_bridge.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +# Copyright 2025 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Test for foxglove bridge import and basic functionality +""" + +import pytest +import threading +import time +import warnings +from unittest.mock import patch, MagicMock + +warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets.server") +warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets.legacy") + + +def test_foxglove_bridge_import(): + """Test that the foxglove bridge can be imported successfully.""" + try: + import dimos_lcm.lcm_foxglove_bridge as bridge + + assert hasattr(bridge, "LcmFoxgloveBridgeRunner") + except ImportError as e: + pytest.fail(f"Failed to import foxglove bridge: {e}") + + +def test_foxglove_bridge_runner_init(): + """Test that LcmFoxgloveBridgeRunner can be initialized with default parameters.""" + try: + import dimos_lcm.lcm_foxglove_bridge as bridge + + runner = bridge.LcmFoxgloveBridgeRunner( + host="localhost", port=8765, debug=False, num_threads=2 + ) + + # Check that the runner was created successfully + assert runner is not None + + except Exception as e: + pytest.fail(f"Failed to initialize LcmFoxgloveBridgeRunner: {e}") + + +def test_foxglove_bridge_runner_params(): + """Test that LcmFoxgloveBridgeRunner accepts various parameter configurations.""" + try: + import dimos_lcm.lcm_foxglove_bridge as bridge + + configs = [ + {"host": "0.0.0.0", "port": 8765, "debug": True, "num_threads": 1}, + {"host": "127.0.0.1", "port": 9090, "debug": False, "num_threads": 4}, + {"host": "localhost", "port": 8080, "debug": True, "num_threads": 2}, + ] + + for config in configs: + runner = bridge.LcmFoxgloveBridgeRunner(**config) + assert runner is not None + + except Exception as e: + pytest.fail(f"Failed to create runner with different configs: {e}") + + +def test_bridge_runner_has_run_method(): + """Test that the bridge runner has a run method that can be called.""" + try: + import dimos_lcm.lcm_foxglove_bridge as bridge + + runner = bridge.LcmFoxgloveBridgeRunner( + host="localhost", port=8765, debug=False, num_threads=1 + ) + + # Check that the run method exists + assert hasattr(runner, "run") + assert callable(getattr(runner, "run")) + + except Exception as e: + pytest.fail(f"Failed to verify run method: {e}") diff --git a/pyproject.toml b/pyproject.toml index 5cb8c5be9d..2d258827a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,10 @@ dependencies = [ # Multiprocess "dask[complete]==2025.5.1", - "lcm_msgs @ git+https://github.com/dimensionalOS/python_lcm_msgs.git@main#egg=lcm_msgs" + + # LCM / DimOS utilities + "dimos-lcm @ git+https://github.com/dimensionalOS/dimos-lcm.git@main", + ] [project.optional-dependencies]