Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion dimos/msgs/geometry_msgs/Pose.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions dimos/msgs/geometry_msgs/PoseStamped.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dimos/msgs/geometry_msgs/Quaternion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dimos/msgs/geometry_msgs/Vector3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dimos/msgs/geometry_msgs/test_Pose.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dimos/msgs/geometry_msgs/test_Quaternion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions dimos/msgs/sensor_msgs/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions dimos/msgs/sensor_msgs/PointCloud2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
49 changes: 49 additions & 0 deletions dimos/robot/foxglove_bridge.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 6 additions & 0 deletions dimos/robot/unitree_webrtc/multiprocess/unitree_go2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand All @@ -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()
Expand Down
58 changes: 58 additions & 0 deletions dimos/utils/run_foxglove_bridge.py
Original file line number Diff line number Diff line change
@@ -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()
89 changes: 89 additions & 0 deletions dimos/utils/test_foxglove_bridge.py
Original file line number Diff line number Diff line change
@@ -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}")
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down