From f6c33a21e7b8355793cf45e6f96bf5d9097bc6cc Mon Sep 17 00:00:00 2001 From: ruthwikdasyam Date: Fri, 6 Mar 2026 17:15:24 -0800 Subject: [PATCH 01/14] feat: obstacle avoid off + balance stand for go2 --- dimos/core/global_config.py | 1 + dimos/robot/unitree/connection.py | 11 ++++++ dimos/robot/unitree/go2/connection.py | 43 ++++++++++++++++-------- dimos/robot/unitree/mujoco_connection.py | 6 ++++ 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/dimos/core/global_config.py b/dimos/core/global_config.py index 15c37186ac..e3f95395d0 100644 --- a/dimos/core/global_config.py +++ b/dimos/core/global_config.py @@ -46,6 +46,7 @@ class GlobalConfig(BaseSettings): planner_strategy: NavigationStrategy = "simple" planner_robot_speed: float | None = None dtop: bool = False + disable_obstacle_avoidance: bool = False model_config = SettingsConfigDict( env_file=".env", diff --git a/dimos/robot/unitree/connection.py b/dimos/robot/unitree/connection.py index f3f8ffaafb..5672a659a1 100644 --- a/dimos/robot/unitree/connection.py +++ b/dimos/robot/unitree/connection.py @@ -277,6 +277,17 @@ def lowstate_stream(self) -> Observable[LowStateMsg]: def standup(self) -> bool: return bool(self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["StandUp"]})) + def balance_stand(self) -> bool: + return bool( + self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["BalanceStand"]}) + ) + + def disable_obstacle_avoidance(self) -> None: + self.publish_request( + RTC_TOPIC["OBSTACLES_AVOID"], + {"api_id": 1001, "parameter": {"enable": 0}}, + ) + def liedown(self) -> bool: return bool( self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["StandDown"]}) diff --git a/dimos/robot/unitree/go2/connection.py b/dimos/robot/unitree/go2/connection.py index 4c7d4755fb..b8ec6eefb2 100644 --- a/dimos/robot/unitree/go2/connection.py +++ b/dimos/robot/unitree/go2/connection.py @@ -60,6 +60,8 @@ def video_stream(self) -> Observable: ... # type: ignore[type-arg] def move(self, twist: Twist, duration: float = 0.0) -> bool: ... def standup(self) -> bool: ... def liedown(self) -> bool: ... + def balance_stand(self) -> None: ... + def disable_obstacle_avoidance(self) -> None: ... def publish_request(self, topic: str, data: dict) -> dict: ... # type: ignore[type-arg] @@ -81,6 +83,21 @@ def _camera_info_static() -> CameraInfo: ) +def make_connection(ip: str | None, cfg: GlobalConfig) -> Go2ConnectionProtocol: + """Create a Go2 connection backend based on IP and config.""" + connection_type = cfg.unitree_connection_type + + if ip in ("fake", "mock", "replay") or connection_type == "replay": + return ReplayConnection() + elif ip == "mujoco" or connection_type == "mujoco": + from dimos.robot.unitree.mujoco_connection import MujocoConnection + + return MujocoConnection(cfg) + else: + assert ip is not None, "IP address must be provided for hardware connection" + return UnitreeWebRTCConnection(ip) + + class ReplayConnection(UnitreeWebRTCConnection): dir_name = "unitree_go2_bigoffice" @@ -108,6 +125,12 @@ def standup(self) -> bool: def liedown(self) -> bool: return True + def balance_stand(self) -> None: + pass + + def disable_obstacle_avoidance(self) -> None: + pass + @simple_mcache def lidar_stream(self): # type: ignore[no-untyped-def] lidar_store = TimedSensorReplay(f"{self.dir_name}/lidar") # type: ignore[var-annotated] @@ -181,18 +204,7 @@ def __init__( # type: ignore[no-untyped-def] self._global_config = cfg ip = ip if ip is not None else self._global_config.robot_ip - - connection_type = self._global_config.unitree_connection_type - - if ip in ["fake", "mock", "replay"] or connection_type == "replay": - self.connection = ReplayConnection() - elif ip == "mujoco" or connection_type == "mujoco": - from dimos.robot.unitree.mujoco_connection import MujocoConnection - - self.connection = MujocoConnection(self._global_config) - else: - assert ip is not None, "IP address must be provided" - self.connection = UnitreeWebRTCConnection(ip) + self.connection = make_connection(ip, self._global_config) Module.__init__(self, *args, **kwargs) @@ -229,7 +241,10 @@ def onimage(image: Image) -> None: self._camera_info_thread.start() self.standup() - # self.record("go2_bigoffice") + time.sleep(3) + self.connection.balance_stand() + if self._global_config.disable_obstacle_avoidance: + self.connection.disable_obstacle_avoidance() @rpc def stop(self) -> None: @@ -337,4 +352,4 @@ def deploy(dimos: ModuleCoordinator, ip: str, prefix: str = "") -> "ModuleProxy" return connection -__all__ = ["GO2Connection", "deploy", "go2_connection"] +__all__ = ["GO2Connection", "deploy", "go2_connection", "make_connection"] diff --git a/dimos/robot/unitree/mujoco_connection.py b/dimos/robot/unitree/mujoco_connection.py index f998ae1dd9..d649b89ab5 100644 --- a/dimos/robot/unitree/mujoco_connection.py +++ b/dimos/robot/unitree/mujoco_connection.py @@ -229,6 +229,12 @@ def standup(self) -> bool: def liedown(self) -> bool: return True + def balance_stand(self) -> None: + pass + + def disable_obstacle_avoidance(self) -> None: + pass + def get_video_frame(self) -> NDArray[Any] | None: if self.shm_data is None: return None From 2f1afbe27b776eee0f52e3e6762a938cb4b9b234 Mon Sep 17 00:00:00 2001 From: ruthwikdasyam Date: Fri, 6 Mar 2026 17:15:37 -0800 Subject: [PATCH 02/14] feat: connection to multiple go2 --- dimos/robot/unitree/go2/fleet_connection.py | 141 ++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 dimos/robot/unitree/go2/fleet_connection.py diff --git a/dimos/robot/unitree/go2/fleet_connection.py b/dimos/robot/unitree/go2/fleet_connection.py new file mode 100644 index 0000000000..c845337905 --- /dev/null +++ b/dimos/robot/unitree/go2/fleet_connection.py @@ -0,0 +1,141 @@ +# Copyright 2025-2026 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. + +"""Go2 Fleet Connection - manage multiple Go2 robots as a fleet""" + +from __future__ import annotations + +import logging +import time +from typing import TYPE_CHECKING, Any + +from dimos.core.core import rpc +from dimos.core.global_config import GlobalConfig, global_config +from dimos.core.module_coordinator import ModuleCoordinator +from dimos.core.transport import LCMTransport, pSHMTransport +from dimos.msgs.geometry_msgs import Twist +from dimos.msgs.sensor_msgs import CameraInfo +from dimos.robot.unitree.go2.connection import ( + GO2Connection, + Go2ConnectionProtocol, + make_connection, +) + +if TYPE_CHECKING: + from dimos.core.rpc_client import ModuleProxy + +logger = logging.getLogger(__name__) + + +class Go2FleetConnection(GO2Connection): + """Multiple Go2 robots as a fleet. + + Inherits all single-robot behaviour from GO2Connection for the primary + (first) robot. Additional robots only receive broadcast commands + (move, standup, liedown, publish_request). + """ + + def __init__( + self, + ips: list[str], + cfg: GlobalConfig = global_config, + *args: object, + **kwargs: object, + ) -> None: + if not ips: + raise ValueError("At least one IP address is required") + self._extra_ips = ips[1:] + self._extra_connections: list[Go2ConnectionProtocol] = [] + # Primary robot handled by parent + super().__init__(ip=ips[0], cfg=cfg, *args, **kwargs) + + @rpc + def start(self) -> None: + # Start extra connections before parent starts the primary + for ip in self._extra_ips: + logger.info(f"Connecting to fleet Go2 at {ip}...") + conn = make_connection(ip, self._global_config) + conn.start() + self._extra_connections.append(conn) + + # Parent starts primary robot, subscribes sensors, stands up, balance stands + super().start() + + # Stand up and configure extra robots + for conn in self._extra_connections: + conn.standup() + time.sleep(3) + for conn in self._extra_connections: + conn.balance_stand() + if self._global_config.disable_obstacle_avoidance: + conn.disable_obstacle_avoidance() + + @rpc + def stop(self) -> None: + for conn in self._extra_connections: + try: + conn.liedown() + conn.stop() + except Exception as e: + logger.error(f"Error stopping fleet Go2: {e}") + self._extra_connections.clear() + super().stop() + + @property + def _all_connections(self) -> list[Go2ConnectionProtocol]: + return [self.connection] + self._extra_connections + + @rpc + def move(self, twist: Twist, duration: float = 0.0) -> bool: + """Send movement command to all robots.""" + return all(conn.move(twist, duration) for conn in self._all_connections) + + @rpc + def standup(self) -> bool: + """Make all robots stand up.""" + return all(conn.standup() for conn in self._all_connections) + + @rpc + def liedown(self) -> bool: + """Make all robots lie down.""" + return all(conn.liedown() for conn in self._all_connections) + + @rpc + def publish_request(self, topic: str, data: dict[str, Any]) -> list[dict[Any, Any]]: + """Publish a request to all robots.""" + return [conn.publish_request(topic, data) for conn in self._all_connections] + + +go2_fleet_connection = Go2FleetConnection.blueprint + + +def deploy(dimos: ModuleCoordinator, ips: list[str], prefix: str = "") -> "ModuleProxy": + from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE + + connection = dimos.deploy(Go2FleetConnection, ips) # type: ignore[attr-defined] + + connection.pointcloud.transport = pSHMTransport( + f"{prefix}/lidar", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ) + connection.color_image.transport = pSHMTransport( + f"{prefix}/image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE + ) + connection.cmd_vel.transport = LCMTransport(f"{prefix}/cmd_vel", Twist) + connection.camera_info.transport = LCMTransport(f"{prefix}/camera_info", CameraInfo) + connection.start() + + return connection + + +__all__ = ["Go2FleetConnection", "deploy", "go2_fleet_connection"] From f76fe0fc5617851bee79400313c8d8cabb9d6c5a Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 7 Mar 2026 14:06:18 -0800 Subject: [PATCH 03/14] feat: robot_ips env variable --- dimos/core/global_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dimos/core/global_config.py b/dimos/core/global_config.py index e3f95395d0..ff3bbba3e8 100644 --- a/dimos/core/global_config.py +++ b/dimos/core/global_config.py @@ -28,6 +28,7 @@ def _get_all_numbers(s: str) -> list[float]: class GlobalConfig(BaseSettings): robot_ip: str | None = None + robot_ips: str | None = None simulation: bool = False replay: bool = False viewer_backend: ViewerBackend = "rerun-web" From fe4902501f200cb52dfa2623c94804df1d16691c Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 7 Mar 2026 14:07:45 -0800 Subject: [PATCH 04/14] feat: fleet connection --- dimos/robot/unitree/go2/fleet_connection.py | 34 +++++++++------------ 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/dimos/robot/unitree/go2/fleet_connection.py b/dimos/robot/unitree/go2/fleet_connection.py index c845337905..cd6a10f701 100644 --- a/dimos/robot/unitree/go2/fleet_connection.py +++ b/dimos/robot/unitree/go2/fleet_connection.py @@ -22,7 +22,6 @@ from dimos.core.core import rpc from dimos.core.global_config import GlobalConfig, global_config -from dimos.core.module_coordinator import ModuleCoordinator from dimos.core.transport import LCMTransport, pSHMTransport from dimos.msgs.geometry_msgs import Twist from dimos.msgs.sensor_msgs import CameraInfo @@ -33,28 +32,32 @@ ) if TYPE_CHECKING: + from dimos.core.module_coordinator import ModuleCoordinator from dimos.core.rpc_client import ModuleProxy logger = logging.getLogger(__name__) class Go2FleetConnection(GO2Connection): - """Multiple Go2 robots as a fleet. - - Inherits all single-robot behaviour from GO2Connection for the primary + """Inherits all single-robot behaviour from GO2Connection for the primary (first) robot. Additional robots only receive broadcast commands (move, standup, liedown, publish_request). """ def __init__( self, - ips: list[str], + ips: list[str] | None = None, cfg: GlobalConfig = global_config, *args: object, **kwargs: object, ) -> None: if not ips: - raise ValueError("At least one IP address is required") + raw = cfg.robot_ips + if not raw: + raise ValueError( + "No IPs provided. Pass ips= or set ROBOT_IPS (e.g. ROBOT_IPS=10.0.0.102,10.0.0.209)" + ) + ips = [ip.strip() for ip in raw.split(",") if ip.strip()] self._extra_ips = ips[1:] self._extra_connections: list[Go2ConnectionProtocol] = [] # Primary robot handled by parent @@ -62,17 +65,15 @@ def __init__( @rpc def start(self) -> None: - # Start extra connections before parent starts the primary for ip in self._extra_ips: logger.info(f"Connecting to fleet Go2 at {ip}...") conn = make_connection(ip, self._global_config) conn.start() self._extra_connections.append(conn) - # Parent starts primary robot, subscribes sensors, stands up, balance stands + # Parent starts primary robot, subscribes sensors super().start() - # Stand up and configure extra robots for conn in self._extra_connections: conn.standup() time.sleep(3) @@ -84,31 +85,24 @@ def start(self) -> None: @rpc def stop(self) -> None: for conn in self._extra_connections: - try: - conn.liedown() - conn.stop() - except Exception as e: - logger.error(f"Error stopping fleet Go2: {e}") + conn.stop() self._extra_connections.clear() super().stop() @property def _all_connections(self) -> list[Go2ConnectionProtocol]: - return [self.connection] + self._extra_connections + return [self.connection, *self._extra_connections] @rpc def move(self, twist: Twist, duration: float = 0.0) -> bool: - """Send movement command to all robots.""" return all(conn.move(twist, duration) for conn in self._all_connections) @rpc def standup(self) -> bool: - """Make all robots stand up.""" return all(conn.standup() for conn in self._all_connections) @rpc def liedown(self) -> bool: - """Make all robots lie down.""" return all(conn.liedown() for conn in self._all_connections) @rpc @@ -120,10 +114,10 @@ def publish_request(self, topic: str, data: dict[str, Any]) -> list[dict[Any, An go2_fleet_connection = Go2FleetConnection.blueprint -def deploy(dimos: ModuleCoordinator, ips: list[str], prefix: str = "") -> "ModuleProxy": +def deploy(dimos: ModuleCoordinator, ips: list[str], prefix: str = "") -> ModuleProxy: from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE - connection = dimos.deploy(Go2FleetConnection, ips) # type: ignore[attr-defined] + connection = dimos.deploy(Go2FleetConnection, ips) connection.pointcloud.transport = pSHMTransport( f"{prefix}/lidar", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE From eae4d355e69b838d09d1d4c0ba94984938853087 Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 7 Mar 2026 14:08:16 -0800 Subject: [PATCH 05/14] feat: fleet blueprints --- dimos/robot/all_blueprints.py | 1 + .../go2/blueprints/basic/unitree_go2_fleet.py | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 dimos/robot/unitree/go2/blueprints/basic/unitree_go2_fleet.py diff --git a/dimos/robot/all_blueprints.py b/dimos/robot/all_blueprints.py index 6026572388..e37a0be045 100644 --- a/dimos/robot/all_blueprints.py +++ b/dimos/robot/all_blueprints.py @@ -76,6 +76,7 @@ "unitree-go2-agentic-mcp": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_agentic_mcp:unitree_go2_agentic_mcp", "unitree-go2-agentic-ollama": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_agentic_ollama:unitree_go2_agentic_ollama", "unitree-go2-basic": "dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic:unitree_go2_basic", + "unitree-go2-fleet": "dimos.robot.unitree.go2.blueprints.basic.unitree_go2_fleet:unitree_go2_fleet", "unitree-go2-detection": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_detection:unitree_go2_detection", "unitree-go2-ros": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_ros:unitree_go2_ros", "unitree-go2-spatial": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_spatial:unitree_go2_spatial", diff --git a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_fleet.py b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_fleet.py new file mode 100644 index 0000000000..015cfcdba4 --- /dev/null +++ b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_fleet.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +# Copyright 2025-2026 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. + +"""Blueprint for Go2 fleet — multiple Go2 robots controlled together. + +Usage: + ROBOT_IPS=10.0.0.102,10.0.0.209 dimos run unitree-go2-fleet +""" + +from dimos.core.blueprints import autoconnect +from dimos.protocol.service.system_configurator import ClockSyncConfigurator +from dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic import with_vis +from dimos.robot.unitree.go2.fleet_connection import go2_fleet_connection +from dimos.web.websocket_vis.websocket_vis_module import websocket_vis + +unitree_go2_fleet = ( + autoconnect( + with_vis, + go2_fleet_connection(), + websocket_vis(), + ) + .global_config(n_workers=4, robot_model="unitree_go2") + .configurators(ClockSyncConfigurator()) +) + +__all__ = ["unitree_go2_fleet"] From 8729005d311849faeed47c2e99f3ebc588fb8ae3 Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 7 Mar 2026 14:22:56 -0800 Subject: [PATCH 06/14] misc: fixes --- dimos/robot/unitree/go2/connection.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dimos/robot/unitree/go2/connection.py b/dimos/robot/unitree/go2/connection.py index b8ec6eefb2..34fac839e0 100644 --- a/dimos/robot/unitree/go2/connection.py +++ b/dimos/robot/unitree/go2/connection.py @@ -84,7 +84,6 @@ def _camera_info_static() -> CameraInfo: def make_connection(ip: str | None, cfg: GlobalConfig) -> Go2ConnectionProtocol: - """Create a Go2 connection backend based on IP and config.""" connection_type = cfg.unitree_connection_type if ip in ("fake", "mock", "replay") or connection_type == "replay": @@ -94,7 +93,7 @@ def make_connection(ip: str | None, cfg: GlobalConfig) -> Go2ConnectionProtocol: return MujocoConnection(cfg) else: - assert ip is not None, "IP address must be provided for hardware connection" + assert ip is not None, "IP address must be provided" return UnitreeWebRTCConnection(ip) @@ -245,6 +244,8 @@ def onimage(image: Image) -> None: self.connection.balance_stand() if self._global_config.disable_obstacle_avoidance: self.connection.disable_obstacle_avoidance() + + # self.record("go2_bigoffice") @rpc def stop(self) -> None: From 0ea0a11934006ad933a5ddaf8cd53f6a2091fc97 Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 7 Mar 2026 14:23:14 -0800 Subject: [PATCH 07/14] fix: pre-commit issues --- dimos/robot/unitree/go2/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dimos/robot/unitree/go2/connection.py b/dimos/robot/unitree/go2/connection.py index 34fac839e0..16f2a1b4d5 100644 --- a/dimos/robot/unitree/go2/connection.py +++ b/dimos/robot/unitree/go2/connection.py @@ -244,7 +244,7 @@ def onimage(image: Image) -> None: self.connection.balance_stand() if self._global_config.disable_obstacle_avoidance: self.connection.disable_obstacle_avoidance() - + # self.record("go2_bigoffice") @rpc From 90319620b99b8e305975a98cf3bb076cd95369fa Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 7 Mar 2026 14:34:17 -0800 Subject: [PATCH 08/14] fix: try except every error --- dimos/robot/unitree/go2/fleet_connection.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dimos/robot/unitree/go2/fleet_connection.py b/dimos/robot/unitree/go2/fleet_connection.py index cd6a10f701..b31a30fba1 100644 --- a/dimos/robot/unitree/go2/fleet_connection.py +++ b/dimos/robot/unitree/go2/fleet_connection.py @@ -84,8 +84,12 @@ def start(self) -> None: @rpc def stop(self) -> None: + # one robot's error should not prevent others from stopping for conn in self._extra_connections: - conn.stop() + try: + conn.stop() + except Exception as e: + logger.error(f"Error stopping fleet Go2: {e}") self._extra_connections.clear() super().stop() From 75d6053c138f4995d0a0c48d514a17a6a42d481b Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 7 Mar 2026 15:27:16 -0800 Subject: [PATCH 09/14] fix: greptile issues --- dimos/robot/unitree/go2/connection.py | 2 +- dimos/robot/unitree/go2/fleet_connection.py | 24 ++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/dimos/robot/unitree/go2/connection.py b/dimos/robot/unitree/go2/connection.py index 16f2a1b4d5..16a3350d93 100644 --- a/dimos/robot/unitree/go2/connection.py +++ b/dimos/robot/unitree/go2/connection.py @@ -60,7 +60,7 @@ def video_stream(self) -> Observable: ... # type: ignore[type-arg] def move(self, twist: Twist, duration: float = 0.0) -> bool: ... def standup(self) -> bool: ... def liedown(self) -> bool: ... - def balance_stand(self) -> None: ... + def balance_stand(self) -> bool: ... def disable_obstacle_avoidance(self) -> None: ... def publish_request(self, topic: str, data: dict) -> dict: ... # type: ignore[type-arg] diff --git a/dimos/robot/unitree/go2/fleet_connection.py b/dimos/robot/unitree/go2/fleet_connection.py index b31a30fba1..702431c397 100644 --- a/dimos/robot/unitree/go2/fleet_connection.py +++ b/dimos/robot/unitree/go2/fleet_connection.py @@ -17,7 +17,6 @@ from __future__ import annotations import logging -import time from typing import TYPE_CHECKING, Any from dimos.core.core import rpc @@ -71,12 +70,8 @@ def start(self) -> None: conn.start() self._extra_connections.append(conn) - # Parent starts primary robot, subscribes sensors + # Parent starts primary robot, subscribes sensors, calls standup() on all super().start() - - for conn in self._extra_connections: - conn.standup() - time.sleep(3) for conn in self._extra_connections: conn.balance_stand() if self._global_config.disable_obstacle_avoidance: @@ -99,20 +94,25 @@ def _all_connections(self) -> list[Go2ConnectionProtocol]: @rpc def move(self, twist: Twist, duration: float = 0.0) -> bool: - return all(conn.move(twist, duration) for conn in self._all_connections) + results = [conn.move(twist, duration) for conn in self._all_connections] + return all(results) @rpc def standup(self) -> bool: - return all(conn.standup() for conn in self._all_connections) + results = [conn.standup() for conn in self._all_connections] + return all(results) @rpc def liedown(self) -> bool: - return all(conn.liedown() for conn in self._all_connections) + results = [conn.liedown() for conn in self._all_connections] + return all(results) @rpc - def publish_request(self, topic: str, data: dict[str, Any]) -> list[dict[Any, Any]]: - """Publish a request to all robots.""" - return [conn.publish_request(topic, data) for conn in self._all_connections] + def publish_request(self, topic: str, data: dict[str, Any]) -> dict[Any, Any]: + """Publish a request to all robots, return primary's response.""" + for conn in self._extra_connections: + conn.publish_request(topic, data) + return self.connection.publish_request(topic, data) go2_fleet_connection = Go2FleetConnection.blueprint From 37fedd372244dfee9d24f345874711ed9134ae4b Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 7 Mar 2026 15:51:25 -0800 Subject: [PATCH 10/14] fix: liedown method missing --- dimos/robot/unitree/go2/connection.py | 4 ++-- dimos/robot/unitree/go2/fleet_connection.py | 4 ++-- dimos/robot/unitree/mujoco_connection.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dimos/robot/unitree/go2/connection.py b/dimos/robot/unitree/go2/connection.py index 16a3350d93..e1841385a7 100644 --- a/dimos/robot/unitree/go2/connection.py +++ b/dimos/robot/unitree/go2/connection.py @@ -124,8 +124,8 @@ def standup(self) -> bool: def liedown(self) -> bool: return True - def balance_stand(self) -> None: - pass + def balance_stand(self) -> bool: + return True def disable_obstacle_avoidance(self) -> None: pass diff --git a/dimos/robot/unitree/go2/fleet_connection.py b/dimos/robot/unitree/go2/fleet_connection.py index 702431c397..e2bc981ebb 100644 --- a/dimos/robot/unitree/go2/fleet_connection.py +++ b/dimos/robot/unitree/go2/fleet_connection.py @@ -59,8 +59,7 @@ def __init__( ips = [ip.strip() for ip in raw.split(",") if ip.strip()] self._extra_ips = ips[1:] self._extra_connections: list[Go2ConnectionProtocol] = [] - # Primary robot handled by parent - super().__init__(ip=ips[0], cfg=cfg, *args, **kwargs) + super().__init__(*args, ip=ips[0], cfg=cfg, **kwargs) @rpc def start(self) -> None: @@ -82,6 +81,7 @@ def stop(self) -> None: # one robot's error should not prevent others from stopping for conn in self._extra_connections: try: + conn.liedown() conn.stop() except Exception as e: logger.error(f"Error stopping fleet Go2: {e}") diff --git a/dimos/robot/unitree/mujoco_connection.py b/dimos/robot/unitree/mujoco_connection.py index d649b89ab5..b6932e3cf0 100644 --- a/dimos/robot/unitree/mujoco_connection.py +++ b/dimos/robot/unitree/mujoco_connection.py @@ -229,8 +229,8 @@ def standup(self) -> bool: def liedown(self) -> bool: return True - def balance_stand(self) -> None: - pass + def balance_stand(self) -> bool: + return True def disable_obstacle_avoidance(self) -> None: pass From b73376fe1de693fa7deb3437bd06d038bd37713c Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 7 Mar 2026 16:52:18 -0800 Subject: [PATCH 11/14] fix: remove deploy --- dimos/robot/all_blueprints.py | 3 +- dimos/robot/unitree/go2/fleet_connection.py | 57 +++++++++------------ 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/dimos/robot/all_blueprints.py b/dimos/robot/all_blueprints.py index e37a0be045..a2566b79fe 100644 --- a/dimos/robot/all_blueprints.py +++ b/dimos/robot/all_blueprints.py @@ -76,8 +76,8 @@ "unitree-go2-agentic-mcp": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_agentic_mcp:unitree_go2_agentic_mcp", "unitree-go2-agentic-ollama": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_agentic_ollama:unitree_go2_agentic_ollama", "unitree-go2-basic": "dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic:unitree_go2_basic", - "unitree-go2-fleet": "dimos.robot.unitree.go2.blueprints.basic.unitree_go2_fleet:unitree_go2_fleet", "unitree-go2-detection": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_detection:unitree_go2_detection", + "unitree-go2-fleet": "dimos.robot.unitree.go2.blueprints.basic.unitree_go2_fleet:unitree_go2_fleet", "unitree-go2-ros": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_ros:unitree_go2_ros", "unitree-go2-spatial": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_spatial:unitree_go2_spatial", "unitree-go2-temporal-memory": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_temporal_memory:unitree_go2_temporal_memory", @@ -108,6 +108,7 @@ "g1-sim-connection": "dimos.robot.unitree.g1.sim", "g1-skills": "dimos.robot.unitree.g1.skill_container", "go2-connection": "dimos.robot.unitree.go2.connection", + "go2-fleet-connection": "dimos.robot.unitree.go2.fleet_connection", "google-maps-skill": "dimos.agents.skills.google_maps_skill_container", "gps-nav-skill": "dimos.agents.skills.gps_nav_skill", "grasping-module": "dimos.manipulation.grasping.grasping", diff --git a/dimos/robot/unitree/go2/fleet_connection.py b/dimos/robot/unitree/go2/fleet_connection.py index e2bc981ebb..58d8e98fb5 100644 --- a/dimos/robot/unitree/go2/fleet_connection.py +++ b/dimos/robot/unitree/go2/fleet_connection.py @@ -21,9 +21,6 @@ from dimos.core.core import rpc from dimos.core.global_config import GlobalConfig, global_config -from dimos.core.transport import LCMTransport, pSHMTransport -from dimos.msgs.geometry_msgs import Twist -from dimos.msgs.sensor_msgs import CameraInfo from dimos.robot.unitree.go2.connection import ( GO2Connection, Go2ConnectionProtocol, @@ -31,8 +28,7 @@ ) if TYPE_CHECKING: - from dimos.core.module_coordinator import ModuleCoordinator - from dimos.core.rpc_client import ModuleProxy + from dimos.msgs.geometry_msgs import Twist logger = logging.getLogger(__name__) @@ -59,10 +55,11 @@ def __init__( ips = [ip.strip() for ip in raw.split(",") if ip.strip()] self._extra_ips = ips[1:] self._extra_connections: list[Go2ConnectionProtocol] = [] - super().__init__(*args, ip=ips[0], cfg=cfg, **kwargs) + super().__init__(ips[0], cfg, *args, **kwargs) @rpc def start(self) -> None: + self._extra_connections.clear() for ip in self._extra_ips: logger.info(f"Connecting to fleet Go2 at {ip}...") conn = make_connection(ip, self._global_config) @@ -82,6 +79,9 @@ def stop(self) -> None: for conn in self._extra_connections: try: conn.liedown() + except Exception as e: + logger.error(f"Error lying down fleet Go2: {e}") + try: conn.stop() except Exception as e: logger.error(f"Error stopping fleet Go2: {e}") @@ -92,48 +92,41 @@ def stop(self) -> None: def _all_connections(self) -> list[Go2ConnectionProtocol]: return [self.connection, *self._extra_connections] + def _broadcast(self, method: str, *args: object, **kwargs: object) -> list[bool]: + """Call a method on all connections, isolating errors per robot.""" + results: list[bool] = [] + for conn in self._all_connections: + try: + results.append(getattr(conn, method)(*args, **kwargs)) + except Exception as e: + logger.error(f"Fleet {method} failed: {e}") + results.append(False) + return results + @rpc def move(self, twist: Twist, duration: float = 0.0) -> bool: - results = [conn.move(twist, duration) for conn in self._all_connections] - return all(results) + return all(self._broadcast("move", twist, duration)) @rpc def standup(self) -> bool: - results = [conn.standup() for conn in self._all_connections] - return all(results) + return all(self._broadcast("standup")) @rpc def liedown(self) -> bool: - results = [conn.liedown() for conn in self._all_connections] - return all(results) + return all(self._broadcast("liedown")) @rpc def publish_request(self, topic: str, data: dict[str, Any]) -> dict[Any, Any]: """Publish a request to all robots, return primary's response.""" for conn in self._extra_connections: - conn.publish_request(topic, data) + try: + conn.publish_request(topic, data) + except Exception as e: + logger.error(f"Fleet publish_request failed: {e}") return self.connection.publish_request(topic, data) go2_fleet_connection = Go2FleetConnection.blueprint -def deploy(dimos: ModuleCoordinator, ips: list[str], prefix: str = "") -> ModuleProxy: - from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE - - connection = dimos.deploy(Go2FleetConnection, ips) - - connection.pointcloud.transport = pSHMTransport( - f"{prefix}/lidar", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE - ) - connection.color_image.transport = pSHMTransport( - f"{prefix}/image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE - ) - connection.cmd_vel.transport = LCMTransport(f"{prefix}/cmd_vel", Twist) - connection.camera_info.transport = LCMTransport(f"{prefix}/camera_info", CameraInfo) - connection.start() - - return connection - - -__all__ = ["Go2FleetConnection", "deploy", "go2_fleet_connection"] +__all__ = ["Go2FleetConnection", "go2_fleet_connection"] From de8b8cb7c5b4d452ca050635a414be1a153611b8 Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 7 Mar 2026 16:55:33 -0800 Subject: [PATCH 12/14] feat: phone teleop blueprint --- dimos/robot/all_blueprints.py | 1 + dimos/teleop/phone/blueprints.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/dimos/robot/all_blueprints.py b/dimos/robot/all_blueprints.py index a2566b79fe..d02abd61b9 100644 --- a/dimos/robot/all_blueprints.py +++ b/dimos/robot/all_blueprints.py @@ -57,6 +57,7 @@ "mid360-fastlio": "dimos.hardware.sensors.lidar.fastlio2.fastlio_blueprints:mid360_fastlio", "mid360-fastlio-voxels": "dimos.hardware.sensors.lidar.fastlio2.fastlio_blueprints:mid360_fastlio_voxels", "mid360-fastlio-voxels-native": "dimos.hardware.sensors.lidar.fastlio2.fastlio_blueprints:mid360_fastlio_voxels_native", + "phone-go2-fleet-teleop": "dimos.teleop.phone.blueprints:phone_go2_fleet_teleop", "phone-go2-teleop": "dimos.teleop.phone.blueprints:phone_go2_teleop", "simple-phone-teleop": "dimos.teleop.phone.blueprints:simple_phone_teleop", "uintree-g1-primitive-no-nav": "dimos.robot.unitree.g1.blueprints.primitive.uintree_g1_primitive_no_nav:uintree_g1_primitive_no_nav", diff --git a/dimos/teleop/phone/blueprints.py b/dimos/teleop/phone/blueprints.py index 6328af8612..6e68e726e3 100644 --- a/dimos/teleop/phone/blueprints.py +++ b/dimos/teleop/phone/blueprints.py @@ -15,6 +15,7 @@ from dimos.core.blueprints import autoconnect from dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic import unitree_go2_basic +from dimos.robot.unitree.go2.blueprints.basic.unitree_go2_fleet import unitree_go2_fleet from dimos.teleop.phone.phone_extensions import simple_phone_teleop_module # Simple phone teleop (mobile base axis filtering + cmd_vel output) @@ -28,5 +29,11 @@ unitree_go2_basic, ) +# Phone teleop wired to Go2 fleet — twist commands sent to all robots +phone_go2_fleet_teleop = autoconnect( + simple_phone_teleop_module(), + unitree_go2_fleet, +) + -__all__ = ["phone_go2_teleop", "simple_phone_teleop"] +__all__ = ["phone_go2_fleet_teleop", "phone_go2_teleop", "simple_phone_teleop"] From a247600d290cd2d45b978f7588a88fe446348d4b Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 7 Mar 2026 17:18:47 -0800 Subject: [PATCH 13/14] feat: obstcale_avoidance variable --- dimos/core/global_config.py | 2 +- dimos/robot/unitree/connection.py | 4 ++-- dimos/robot/unitree/go2/connection.py | 7 +++---- dimos/robot/unitree/go2/fleet_connection.py | 3 +-- dimos/robot/unitree/mujoco_connection.py | 2 +- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/dimos/core/global_config.py b/dimos/core/global_config.py index ff3bbba3e8..014da1b32b 100644 --- a/dimos/core/global_config.py +++ b/dimos/core/global_config.py @@ -47,7 +47,7 @@ class GlobalConfig(BaseSettings): planner_strategy: NavigationStrategy = "simple" planner_robot_speed: float | None = None dtop: bool = False - disable_obstacle_avoidance: bool = False + obstacle_avoidance: bool = True model_config = SettingsConfigDict( env_file=".env", diff --git a/dimos/robot/unitree/connection.py b/dimos/robot/unitree/connection.py index 5672a659a1..ff73d922ee 100644 --- a/dimos/robot/unitree/connection.py +++ b/dimos/robot/unitree/connection.py @@ -282,10 +282,10 @@ def balance_stand(self) -> bool: self.publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": SPORT_CMD["BalanceStand"]}) ) - def disable_obstacle_avoidance(self) -> None: + def set_obstacle_avoidance(self, enabled: bool = True) -> None: self.publish_request( RTC_TOPIC["OBSTACLES_AVOID"], - {"api_id": 1001, "parameter": {"enable": 0}}, + {"api_id": 1001, "parameter": {"enable": int(enabled)}}, ) def liedown(self) -> bool: diff --git a/dimos/robot/unitree/go2/connection.py b/dimos/robot/unitree/go2/connection.py index e1841385a7..8baeae41f1 100644 --- a/dimos/robot/unitree/go2/connection.py +++ b/dimos/robot/unitree/go2/connection.py @@ -61,7 +61,7 @@ def move(self, twist: Twist, duration: float = 0.0) -> bool: ... def standup(self) -> bool: ... def liedown(self) -> bool: ... def balance_stand(self) -> bool: ... - def disable_obstacle_avoidance(self) -> None: ... + def set_obstacle_avoidance(self, enabled: bool = True) -> None: ... def publish_request(self, topic: str, data: dict) -> dict: ... # type: ignore[type-arg] @@ -127,7 +127,7 @@ def liedown(self) -> bool: def balance_stand(self) -> bool: return True - def disable_obstacle_avoidance(self) -> None: + def set_obstacle_avoidance(self, enabled: bool = True) -> None: pass @simple_mcache @@ -242,8 +242,7 @@ def onimage(image: Image) -> None: self.standup() time.sleep(3) self.connection.balance_stand() - if self._global_config.disable_obstacle_avoidance: - self.connection.disable_obstacle_avoidance() + self.connection.set_obstacle_avoidance(self._global_config.obstacle_avoidance) # self.record("go2_bigoffice") diff --git a/dimos/robot/unitree/go2/fleet_connection.py b/dimos/robot/unitree/go2/fleet_connection.py index 58d8e98fb5..f8d49474cc 100644 --- a/dimos/robot/unitree/go2/fleet_connection.py +++ b/dimos/robot/unitree/go2/fleet_connection.py @@ -70,8 +70,7 @@ def start(self) -> None: super().start() for conn in self._extra_connections: conn.balance_stand() - if self._global_config.disable_obstacle_avoidance: - conn.disable_obstacle_avoidance() + conn.set_obstacle_avoidance(self._global_config.obstacle_avoidance) @rpc def stop(self) -> None: diff --git a/dimos/robot/unitree/mujoco_connection.py b/dimos/robot/unitree/mujoco_connection.py index b6932e3cf0..36673ecb3e 100644 --- a/dimos/robot/unitree/mujoco_connection.py +++ b/dimos/robot/unitree/mujoco_connection.py @@ -232,7 +232,7 @@ def liedown(self) -> bool: def balance_stand(self) -> bool: return True - def disable_obstacle_avoidance(self) -> None: + def set_obstacle_avoidance(self, enabled: bool = True) -> None: pass def get_video_frame(self) -> NDArray[Any] | None: From 08b2e92259dd9718826fc92c2db55866901047b1 Mon Sep 17 00:00:00 2001 From: Ruthwik Date: Sat, 7 Mar 2026 18:01:45 -0800 Subject: [PATCH 14/14] fix: correct logging + remove broadcast method --- dimos/robot/unitree/go2/fleet_connection.py | 37 +++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/dimos/robot/unitree/go2/fleet_connection.py b/dimos/robot/unitree/go2/fleet_connection.py index f8d49474cc..4dd2be2984 100644 --- a/dimos/robot/unitree/go2/fleet_connection.py +++ b/dimos/robot/unitree/go2/fleet_connection.py @@ -16,7 +16,6 @@ from __future__ import annotations -import logging from typing import TYPE_CHECKING, Any from dimos.core.core import rpc @@ -26,11 +25,12 @@ Go2ConnectionProtocol, make_connection, ) +from dimos.utils.logging_config import setup_logger if TYPE_CHECKING: from dimos.msgs.geometry_msgs import Twist -logger = logging.getLogger(__name__) +logger = setup_logger() class Go2FleetConnection(GO2Connection): @@ -61,7 +61,6 @@ def __init__( def start(self) -> None: self._extra_connections.clear() for ip in self._extra_ips: - logger.info(f"Connecting to fleet Go2 at {ip}...") conn = make_connection(ip, self._global_config) conn.start() self._extra_connections.append(conn) @@ -91,28 +90,38 @@ def stop(self) -> None: def _all_connections(self) -> list[Go2ConnectionProtocol]: return [self.connection, *self._extra_connections] - def _broadcast(self, method: str, *args: object, **kwargs: object) -> list[bool]: - """Call a method on all connections, isolating errors per robot.""" + @rpc + def move(self, twist: Twist, duration: float = 0.0) -> bool: results: list[bool] = [] for conn in self._all_connections: try: - results.append(getattr(conn, method)(*args, **kwargs)) + results.append(conn.move(twist, duration)) except Exception as e: - logger.error(f"Fleet {method} failed: {e}") + logger.error(f"Fleet move failed: {e}") results.append(False) - return results - - @rpc - def move(self, twist: Twist, duration: float = 0.0) -> bool: - return all(self._broadcast("move", twist, duration)) + return all(results) @rpc def standup(self) -> bool: - return all(self._broadcast("standup")) + results: list[bool] = [] + for conn in self._all_connections: + try: + results.append(conn.standup()) + except Exception as e: + logger.error(f"Fleet standup failed: {e}") + results.append(False) + return all(results) @rpc def liedown(self) -> bool: - return all(self._broadcast("liedown")) + results: list[bool] = [] + for conn in self._all_connections: + try: + results.append(conn.liedown()) + except Exception as e: + logger.error(f"Fleet liedown failed: {e}") + results.append(False) + return all(results) @rpc def publish_request(self, topic: str, data: dict[str, Any]) -> dict[Any, Any]: