From 1cf0045d4f368dac5d1da36d04aed4b3e36093f1 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 14:50:55 -0700 Subject: [PATCH 01/22] make model_path optional --- dimos/robot/config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dimos/robot/config.py b/dimos/robot/config.py index 8889d4f05b..f44b64dbf8 100644 --- a/dimos/robot/config.py +++ b/dimos/robot/config.py @@ -53,7 +53,7 @@ class RobotConfig(BaseModel): # Required fields name: str - model_path: Path + model_path: Path | None = None end_effector_link: str | None = None # Physical dimensions (meters) @@ -115,6 +115,11 @@ def _ensure_prefix(self) -> None: def _ensure_parsed(self) -> ModelDescription: """Parse model lazily on first access.""" if self._parsed is None: + if self.model_path is None: + raise ValueError( + f"RobotConfig '{self.name}' has no model_path — " + "joint/link info is unavailable. Set model_path to a URDF/MJCF." + ) self._parsed = parse_model(self.model_path, self.package_paths, self.xacro_args) self._ensure_prefix() if self.joint_names is None: From c9a426a1fc6625989e92c6c3207f3a4e05d824fc Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 15:04:37 -0700 Subject: [PATCH 02/22] add flowbase robot --- .../diy/flowbase/blueprints/flowbase_nav.py | 79 ++++++++ dimos/robot/diy/flowbase/config.py | 39 ++++ .../robot/diy/flowbase/effector_high_level.py | 169 ++++++++++++++++++ 3 files changed, 287 insertions(+) create mode 100644 dimos/robot/diy/flowbase/blueprints/flowbase_nav.py create mode 100644 dimos/robot/diy/flowbase/config.py create mode 100644 dimos/robot/diy/flowbase/effector_high_level.py diff --git a/dimos/robot/diy/flowbase/blueprints/flowbase_nav.py b/dimos/robot/diy/flowbase/blueprints/flowbase_nav.py new file mode 100644 index 0000000000..d3dfd15569 --- /dev/null +++ b/dimos/robot/diy/flowbase/blueprints/flowbase_nav.py @@ -0,0 +1,79 @@ +# 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. + +from __future__ import annotations + +import os + +from dimos.core.coordination.blueprints import autoconnect +from dimos.core.global_config import global_config +from dimos.hardware.sensors.lidar.fastlio2.module import FastLio2 +from dimos.navigation.movement_manager.movement_manager import MovementManager +from dimos.navigation.nav_stack.main import create_nav_stack, nav_stack_rerun_config +from dimos.robot.diy.flowbase.config import FLOWBASE, LOCAL_PLANNER_PRECOMPUTED_PATHS +from dimos.robot.diy.flowbase.effector_high_level import FlowBaseHighLevel +from dimos.visualization.vis_module import vis_module + +flowbase_nav = ( + autoconnect( + FastLio2.blueprint( + host_ip=os.getenv("LIDAR_HOST_IP", "192.168.1.5"), + lidar_ip=os.getenv("LIDAR_IP", "192.168.1.189"), + mount=FLOWBASE.internal_odom_offsets["mid360_link"], + map_freq=1.0, + config="default.yaml", + ), + create_nav_stack( + planner="simple", + vehicle_height=0.5, + max_speed=0.8, + terrain_analysis={ + "obstacle_height_threshold": 0.15, + "ground_height_threshold": 0.10, + "sensor_range": 20, + }, + local_planner={ + "paths_dir": str(LOCAL_PLANNER_PRECOMPUTED_PATHS), + "publish_free_paths": False, + }, + simple_planner={ + "cell_size": 0.2, + "obstacle_height_threshold": 0.15, + "inflation_radius": 0.3, + "lookahead_distance": 2.0, + "replan_rate": 5.0, + "replan_cooldown": 2.0, + }, + ), + MovementManager.blueprint(), + FlowBaseHighLevel.blueprint(), + vis_module( + global_config.viewer, + rerun_config={ + **nav_stack_rerun_config({"memory_limit": "1GB"}, vis_throttle=0.5), + "rerun_open": "native", + }, + ), + ) + .remappings( + [ + # nav stack needs "registered_scan" + (FastLio2, "lidar", "registered_scan"), + (FastLio2, "global_map", "global_map_fastlio"), + # SimplePlanner / FarPlanner owns way_point — disconnect MovementManager's + (MovementManager, "way_point", "_mgr_way_point_unused"), + ] + ) + .global_config(n_workers=8) +) diff --git a/dimos/robot/diy/flowbase/config.py b/dimos/robot/diy/flowbase/config.py new file mode 100644 index 0000000000..8f8a86db38 --- /dev/null +++ b/dimos/robot/diy/flowbase/config.py @@ -0,0 +1,39 @@ +# 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. + +"""FlowBase physical description and sensor odometry offsets.""" + +from __future__ import annotations + +from dimos.msgs.geometry_msgs.Pose import Pose +from dimos.msgs.geometry_msgs.Quaternion import Quaternion +from dimos.msgs.geometry_msgs.Vector3 import Vector3 +from dimos.robot.config import RobotConfig +from dimos.robot.unitree.g1.config import G1_LOCAL_PLANNER_PRECOMPUTED_PATHS + +DEFAULT_ADDRESS = "172.6.2.20:11323" + +# just as a starting point. May re-compute these later. In principle robot-specific +LOCAL_PLANNER_PRECOMPUTED_PATHS = G1_LOCAL_PLANNER_PRECOMPUTED_PATHS + +FLOWBASE = RobotConfig( + name="flowbase", + height_clearance=2.0, # meters + width_clearance=1.0, + internal_odom_offsets={ + # Mid-360 lidar: 0.20 m forward, 0.20 m right of base center, 0.10 m above ground. + # it is mounted at an angle but the livox driver will handle that automatically + "mid360_link": Pose(0.20, -0.20, 0.10, *Quaternion.from_euler(Vector3(0, 0, 0))), + }, +) diff --git a/dimos/robot/diy/flowbase/effector_high_level.py b/dimos/robot/diy/flowbase/effector_high_level.py new file mode 100644 index 0000000000..7b39ac98a5 --- /dev/null +++ b/dimos/robot/diy/flowbase/effector_high_level.py @@ -0,0 +1,169 @@ +# 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. + +"""FlowBase high-level control via Portal RPC. + +Subscribes to ``cmd_vel`` and forwards each Twist to the FlowBase controller +as a holonomic target velocity. The controller performs the wheel-level +kinematics on-board, so this module hands off ``(vx, vy, wz)`` rather than +computing per-wheel speeds locally. + +Frame convention: FlowBase uses an inverted Y-axis vs. ROS, so ``vy`` and +``wz`` are negated before being sent to the hardware. + + Standard (ROS): FlowBase: + +Y -Y + ↑ ↑ + ───┼──→ +X ───┼──→ +X + | | +""" + +from __future__ import annotations + +import asyncio +from collections.abc import AsyncGenerator +from typing import Any + +import numpy as np +import portal + +from dimos.agents.annotation import skill +from dimos.core.core import rpc +from dimos.core.module import Module, ModuleConfig +from dimos.core.stream import In +from dimos.msgs.geometry_msgs.Twist import Twist +from dimos.msgs.geometry_msgs.Vector3 import Vector3 +from dimos.robot.diy.flowbase.config import DEFAULT_ADDRESS +from dimos.utils.logging_config import setup_logger + +logger = setup_logger() + + +class FlowBaseHighLevelConfig(ModuleConfig): + address: str = DEFAULT_ADDRESS + cmd_vel_timeout: float = 0.2 + + +class FlowBaseHighLevel(Module): + """High-level FlowBase driver — ``cmd_vel`` → Portal RPC → wheel motors. + + Opens a Portal RPC connection to the FlowBase controller in ``main``. The + framework auto-subscribes ``handle_cmd_vel`` to the ``cmd_vel`` stream, and + each Twist is forwarded via ``move``. A watchdog task auto-stops the + platform if no new Twist arrives within ``cmd_vel_timeout`` seconds. + """ + + cmd_vel: In[Twist] + config: FlowBaseHighLevelConfig + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._client: portal.Client | None = None + self._stop_task: asyncio.Task[None] | None = None + self._last_velocities = [0.0, 0.0, 0.0] + + async def main(self) -> AsyncGenerator[None, None]: + self._client = portal.Client(self.config.address) + logger.info(f"Connected to FlowBase at {self.config.address}") + try: + yield + finally: + if self._stop_task is not None and not self._stop_task.done(): + self._stop_task.cancel() + try: + self._send_velocity(0.0, 0.0, 0.0) + except Exception as e: + logger.error(f"Error stopping FlowBase: {e}") + if self._client is not None: + try: + self._client.close() + except Exception: + pass + self._client = None + logger.info("FlowBase high-level connection stopped") + + async def handle_cmd_vel(self, msg: Twist) -> None: + await self.move(msg) + + @rpc + async def move(self, twist: Twist, duration: float = 0.0) -> bool: + """Send a Twist as a holonomic velocity command. + + With ``duration > 0`` the command runs for that many seconds before + auto-stop. With ``duration == 0`` each call rearms a ``cmd_vel_timeout`` + watchdog; if the stream stalls, the platform stops automatically. + """ + if self._client is None: + logger.warning("FlowBase not connected; ignoring move") + return False + + vx, vy, wz = twist.linear.x, twist.linear.y, twist.angular.z + + if self._stop_task is not None and not self._stop_task.done(): + self._stop_task.cancel() + + # Negate vy and wz for FlowBase's inverted Y-axis frame. + # Send before scheduling the watchdog — otherwise it could fire first. + if not self._send_velocity(vx, -vy, -wz): + return False + + self._last_velocities = [vx, vy, wz] + timeout = duration if duration > 0 else self.config.cmd_vel_timeout + self._stop_task = asyncio.create_task(self._auto_stop(timeout)) + return True + + async def _auto_stop(self, delay: float) -> None: + try: + await asyncio.sleep(delay) + except asyncio.CancelledError: + return + try: + self._send_velocity(0.0, 0.0, 0.0) + self._last_velocities = [0.0, 0.0, 0.0] + except Exception as e: + logger.error(f"Auto-stop failed: {e}") + + @rpc + async def get_state(self) -> str: + if self._client is None: + return "DISCONNECTED" + moving = any(abs(v) > 1e-6 for v in self._last_velocities) + return "MOVING" if moving else "STOPPED" + + @skill + async def move_velocity( + self, x: float, y: float = 0.0, yaw: float = 0.0, duration: float = 0.0 + ) -> str: + """Move the FlowBase at the given velocity for ``duration`` seconds.""" + twist = Twist(linear=Vector3(x, y, 0), angular=Vector3(0, 0, yaw)) + await self.move(twist, duration=duration) + return f"Started moving with velocity=({x}, {y}, {yaw}) for {duration} seconds" + + def _send_velocity(self, vx: float, vy: float, wz: float) -> bool: + """Send a raw velocity (already in FlowBase frame) via Portal RPC.""" + if self._client is None: + return False + try: + command = { + "target_velocity": np.array([vx, vy, wz]), + "frame": "local", + } + self._client.set_target_velocity(command).result() + return True + except Exception as e: + logger.error(f"Error sending FlowBase velocity: {e}") + return False + + +__all__ = ["FlowBaseHighLevel", "FlowBaseHighLevelConfig"] From 272833c76255e2e5a8b79325d555e22fcafeee36 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 15:21:40 -0700 Subject: [PATCH 03/22] add alfred --- .../blueprints/alfred_nav.py} | 56 ++++++++++--------- .../robot/diy/{flowbase => alfred}/config.py | 0 .../effector_high_level.py | 49 +++++++--------- 3 files changed, 48 insertions(+), 57 deletions(-) rename dimos/robot/diy/{flowbase/blueprints/flowbase_nav.py => alfred/blueprints/alfred_nav.py} (65%) rename dimos/robot/diy/{flowbase => alfred}/config.py (100%) rename dimos/robot/diy/{flowbase => alfred}/effector_high_level.py (73%) diff --git a/dimos/robot/diy/flowbase/blueprints/flowbase_nav.py b/dimos/robot/diy/alfred/blueprints/alfred_nav.py similarity index 65% rename from dimos/robot/diy/flowbase/blueprints/flowbase_nav.py rename to dimos/robot/diy/alfred/blueprints/alfred_nav.py index d3dfd15569..b2ebf38776 100644 --- a/dimos/robot/diy/flowbase/blueprints/flowbase_nav.py +++ b/dimos/robot/diy/alfred/blueprints/alfred_nav.py @@ -21,43 +21,45 @@ from dimos.hardware.sensors.lidar.fastlio2.module import FastLio2 from dimos.navigation.movement_manager.movement_manager import MovementManager from dimos.navigation.nav_stack.main import create_nav_stack, nav_stack_rerun_config -from dimos.robot.diy.flowbase.config import FLOWBASE, LOCAL_PLANNER_PRECOMPUTED_PATHS -from dimos.robot.diy.flowbase.effector_high_level import FlowBaseHighLevel +from dimos.robot.diy.alfred.config import ALFRED, LOCAL_PLANNER_PRECOMPUTED_PATHS +from dimos.robot.diy.alfred.effector_high_level import AlfredHighLevel from dimos.visualization.vis_module import vis_module -flowbase_nav = ( +nav_config = dict( + planner="simple", + vehicle_height=0.5, + max_speed=0.8, + terrain_analysis={ + "obstacle_height_threshold": 0.15, + "ground_height_threshold": 0.10, + "sensor_range": 20, + }, + local_planner={ + "paths_dir": str(LOCAL_PLANNER_PRECOMPUTED_PATHS), + "publish_free_paths": False, + }, + simple_planner={ + "cell_size": 0.2, + "obstacle_height_threshold": 0.15, + "inflation_radius": 0.3, + "lookahead_distance": 2.0, + "replan_rate": 5.0, + "replan_cooldown": 2.0, + }, +) + +alfred_nav = ( autoconnect( FastLio2.blueprint( host_ip=os.getenv("LIDAR_HOST_IP", "192.168.1.5"), lidar_ip=os.getenv("LIDAR_IP", "192.168.1.189"), - mount=FLOWBASE.internal_odom_offsets["mid360_link"], + mount=ALFRED.internal_odom_offsets["mid360_link"], map_freq=1.0, config="default.yaml", ), - create_nav_stack( - planner="simple", - vehicle_height=0.5, - max_speed=0.8, - terrain_analysis={ - "obstacle_height_threshold": 0.15, - "ground_height_threshold": 0.10, - "sensor_range": 20, - }, - local_planner={ - "paths_dir": str(LOCAL_PLANNER_PRECOMPUTED_PATHS), - "publish_free_paths": False, - }, - simple_planner={ - "cell_size": 0.2, - "obstacle_height_threshold": 0.15, - "inflation_radius": 0.3, - "lookahead_distance": 2.0, - "replan_rate": 5.0, - "replan_cooldown": 2.0, - }, - ), + create_nav_stack(**nav_config), MovementManager.blueprint(), - FlowBaseHighLevel.blueprint(), + AlfredHighLevel.blueprint(), vis_module( global_config.viewer, rerun_config={ diff --git a/dimos/robot/diy/flowbase/config.py b/dimos/robot/diy/alfred/config.py similarity index 100% rename from dimos/robot/diy/flowbase/config.py rename to dimos/robot/diy/alfred/config.py diff --git a/dimos/robot/diy/flowbase/effector_high_level.py b/dimos/robot/diy/alfred/effector_high_level.py similarity index 73% rename from dimos/robot/diy/flowbase/effector_high_level.py rename to dimos/robot/diy/alfred/effector_high_level.py index 7b39ac98a5..2942c3fcb0 100644 --- a/dimos/robot/diy/flowbase/effector_high_level.py +++ b/dimos/robot/diy/alfred/effector_high_level.py @@ -12,17 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""FlowBase high-level control via Portal RPC. +"""Alfred high-level control via Portal RPC. -Subscribes to ``cmd_vel`` and forwards each Twist to the FlowBase controller +Subscribes to ``cmd_vel`` and forwards each Twist to the Alfred controller as a holonomic target velocity. The controller performs the wheel-level kinematics on-board, so this module hands off ``(vx, vy, wz)`` rather than computing per-wheel speeds locally. -Frame convention: FlowBase uses an inverted Y-axis vs. ROS, so ``vy`` and +Frame convention: Alfred uses an inverted Y-axis vs. ROS, so ``vy`` and ``wz`` are negated before being sent to the hardware. - Standard (ROS): FlowBase: + Standard (ROS): Alfred: +Y -Y ↑ ↑ ───┼──→ +X ───┼──→ +X @@ -44,28 +44,20 @@ from dimos.core.stream import In from dimos.msgs.geometry_msgs.Twist import Twist from dimos.msgs.geometry_msgs.Vector3 import Vector3 -from dimos.robot.diy.flowbase.config import DEFAULT_ADDRESS +from dimos.robot.diy.alfred.config import DEFAULT_ADDRESS from dimos.utils.logging_config import setup_logger logger = setup_logger() -class FlowBaseHighLevelConfig(ModuleConfig): +class AlfredHighLevelConfig(ModuleConfig): address: str = DEFAULT_ADDRESS cmd_vel_timeout: float = 0.2 -class FlowBaseHighLevel(Module): - """High-level FlowBase driver — ``cmd_vel`` → Portal RPC → wheel motors. - - Opens a Portal RPC connection to the FlowBase controller in ``main``. The - framework auto-subscribes ``handle_cmd_vel`` to the ``cmd_vel`` stream, and - each Twist is forwarded via ``move``. A watchdog task auto-stops the - platform if no new Twist arrives within ``cmd_vel_timeout`` seconds. - """ - +class AlfredHighLevel(Module): cmd_vel: In[Twist] - config: FlowBaseHighLevelConfig + config: AlfredHighLevelConfig def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) @@ -75,7 +67,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: async def main(self) -> AsyncGenerator[None, None]: self._client = portal.Client(self.config.address) - logger.info(f"Connected to FlowBase at {self.config.address}") + logger.info(f"Connected to Alfred at {self.config.address}") try: yield finally: @@ -84,17 +76,14 @@ async def main(self) -> AsyncGenerator[None, None]: try: self._send_velocity(0.0, 0.0, 0.0) except Exception as e: - logger.error(f"Error stopping FlowBase: {e}") + logger.error(f"Error stopping Alfred: {e}") if self._client is not None: try: self._client.close() except Exception: pass self._client = None - logger.info("FlowBase high-level connection stopped") - - async def handle_cmd_vel(self, msg: Twist) -> None: - await self.move(msg) + logger.info("Alfred high-level connection stopped") @rpc async def move(self, twist: Twist, duration: float = 0.0) -> bool: @@ -105,7 +94,7 @@ async def move(self, twist: Twist, duration: float = 0.0) -> bool: watchdog; if the stream stalls, the platform stops automatically. """ if self._client is None: - logger.warning("FlowBase not connected; ignoring move") + logger.warning("Alfred not connected; ignoring move") return False vx, vy, wz = twist.linear.x, twist.linear.y, twist.angular.z @@ -113,17 +102,17 @@ async def move(self, twist: Twist, duration: float = 0.0) -> bool: if self._stop_task is not None and not self._stop_task.done(): self._stop_task.cancel() - # Negate vy and wz for FlowBase's inverted Y-axis frame. + # Negate vy and wz for Alfred's inverted Y-axis frame. # Send before scheduling the watchdog — otherwise it could fire first. if not self._send_velocity(vx, -vy, -wz): return False self._last_velocities = [vx, vy, wz] timeout = duration if duration > 0 else self.config.cmd_vel_timeout - self._stop_task = asyncio.create_task(self._auto_stop(timeout)) + self._stop_task = asyncio.create_task(self._auto_stop_movement(timeout)) return True - async def _auto_stop(self, delay: float) -> None: + async def _auto_stop_movement(self, delay: float) -> None: try: await asyncio.sleep(delay) except asyncio.CancelledError: @@ -145,13 +134,13 @@ async def get_state(self) -> str: async def move_velocity( self, x: float, y: float = 0.0, yaw: float = 0.0, duration: float = 0.0 ) -> str: - """Move the FlowBase at the given velocity for ``duration`` seconds.""" + """Move the Alfred at the given velocity for ``duration`` seconds.""" twist = Twist(linear=Vector3(x, y, 0), angular=Vector3(0, 0, yaw)) await self.move(twist, duration=duration) return f"Started moving with velocity=({x}, {y}, {yaw}) for {duration} seconds" def _send_velocity(self, vx: float, vy: float, wz: float) -> bool: - """Send a raw velocity (already in FlowBase frame) via Portal RPC.""" + """Send a raw velocity (already in Alfred frame) via Portal RPC.""" if self._client is None: return False try: @@ -162,8 +151,8 @@ def _send_velocity(self, vx: float, vy: float, wz: float) -> bool: self._client.set_target_velocity(command).result() return True except Exception as e: - logger.error(f"Error sending FlowBase velocity: {e}") + logger.error(f"Error sending Alfred velocity: {e}") return False -__all__ = ["FlowBaseHighLevel", "FlowBaseHighLevelConfig"] +__all__ = ["AlfredHighLevel", "AlfredHighLevelConfig"] From 0863ad14ca1635f276f1027b4fd730a8bebc9c0d Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 15:23:13 -0700 Subject: [PATCH 04/22] add blueprint --- dimos/robot/all_blueprints.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dimos/robot/all_blueprints.py b/dimos/robot/all_blueprints.py index ef7214811c..15a44c7bf6 100644 --- a/dimos/robot/all_blueprints.py +++ b/dimos/robot/all_blueprints.py @@ -16,6 +16,7 @@ # Run `pytest dimos/robot/test_all_blueprints_generation.py` to regenerate. all_blueprints = { + "alfred-nav": "dimos.robot.diy.alfred.blueprints.alfred_nav:alfred_nav", "coordinator-basic": "dimos.control.blueprints.basic:coordinator_basic", "coordinator-cartesian-ik-mock": "dimos.control.blueprints.teleop:coordinator_cartesian_ik_mock", "coordinator-cartesian-ik-piper": "dimos.control.blueprints.teleop:coordinator_cartesian_ik_piper", @@ -118,6 +119,7 @@ all_modules = { + "alfred-high-level": "dimos.robot.diy.alfred.effector_high_level.AlfredHighLevel", "arm-teleop-module": "dimos.teleop.quest.quest_extensions.ArmTeleopModule", "b-box-navigation-module": "dimos.navigation.bbox_navigation.BBoxNavigationModule", "b1-connection-module": "dimos.robot.unitree.b1.connection.B1ConnectionModule", From 902041512e9852781ee985b5b27193f64dd7a47c Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 15:25:05 -0700 Subject: [PATCH 05/22] naming --- dimos/robot/diy/alfred/config.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dimos/robot/diy/alfred/config.py b/dimos/robot/diy/alfred/config.py index 8f8a86db38..f8c2eaac36 100644 --- a/dimos/robot/diy/alfred/config.py +++ b/dimos/robot/diy/alfred/config.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""FlowBase physical description and sensor odometry offsets.""" - from __future__ import annotations from dimos.msgs.geometry_msgs.Pose import Pose @@ -28,7 +26,7 @@ LOCAL_PLANNER_PRECOMPUTED_PATHS = G1_LOCAL_PLANNER_PRECOMPUTED_PATHS FLOWBASE = RobotConfig( - name="flowbase", + name="alfred", height_clearance=2.0, # meters width_clearance=1.0, internal_odom_offsets={ From 6ea025164ebde206c781c19e5fb65b51ab30af6c Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 15:28:19 -0700 Subject: [PATCH 06/22] name fix --- dimos/robot/diy/alfred/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dimos/robot/diy/alfred/config.py b/dimos/robot/diy/alfred/config.py index f8c2eaac36..ad2cf96035 100644 --- a/dimos/robot/diy/alfred/config.py +++ b/dimos/robot/diy/alfred/config.py @@ -25,7 +25,7 @@ # just as a starting point. May re-compute these later. In principle robot-specific LOCAL_PLANNER_PRECOMPUTED_PATHS = G1_LOCAL_PLANNER_PRECOMPUTED_PATHS -FLOWBASE = RobotConfig( +ALFRED = RobotConfig( name="alfred", height_clearance=2.0, # meters width_clearance=1.0, From 8e9c445bf3099a9b960c5b62129cf4ab602e14f7 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 15:28:48 -0700 Subject: [PATCH 07/22] mypy --- dimos/robot/diy/alfred/blueprints/alfred_nav.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dimos/robot/diy/alfred/blueprints/alfred_nav.py b/dimos/robot/diy/alfred/blueprints/alfred_nav.py index b2ebf38776..91a8db9bec 100644 --- a/dimos/robot/diy/alfred/blueprints/alfred_nav.py +++ b/dimos/robot/diy/alfred/blueprints/alfred_nav.py @@ -15,6 +15,7 @@ from __future__ import annotations import os +from typing import Any from dimos.core.coordination.blueprints import autoconnect from dimos.core.global_config import global_config @@ -25,7 +26,7 @@ from dimos.robot.diy.alfred.effector_high_level import AlfredHighLevel from dimos.visualization.vis_module import vis_module -nav_config = dict( +nav_config: dict[str, Any] = dict( planner="simple", vehicle_height=0.5, max_speed=0.8, From 277cd29c8a81e3769a7f1226ac447240febb521e Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 15:30:35 -0700 Subject: [PATCH 08/22] mypy --- dimos/robot/config.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dimos/robot/config.py b/dimos/robot/config.py index f44b64dbf8..d1131b0301 100644 --- a/dimos/robot/config.py +++ b/dimos/robot/config.py @@ -198,6 +198,11 @@ def to_robot_model_config(self) -> RobotModelConfig: f"RobotConfig '{self.name}' has no end_effector_link — " "cannot generate RobotModelConfig for manipulation." ) + if self.model_path is None: + raise ValueError( + f"RobotConfig '{self.name}' has no model_path — " + "cannot generate RobotModelConfig for manipulation." + ) bp = self.base_pose base_pose = PoseStamped( position=Vector3(x=bp[0], y=bp[1], z=bp[2]), From f6010b0e9c5ac4e74b247be86acc25e586e0f7c8 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 16:28:06 -0700 Subject: [PATCH 09/22] - --- dimos/robot/diy/alfred/config.py | 2 +- dimos/robot/diy/alfred/effector_high_level.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dimos/robot/diy/alfred/config.py b/dimos/robot/diy/alfred/config.py index ad2cf96035..f9645ab2b7 100644 --- a/dimos/robot/diy/alfred/config.py +++ b/dimos/robot/diy/alfred/config.py @@ -32,6 +32,6 @@ internal_odom_offsets={ # Mid-360 lidar: 0.20 m forward, 0.20 m right of base center, 0.10 m above ground. # it is mounted at an angle but the livox driver will handle that automatically - "mid360_link": Pose(0.20, -0.20, 0.10, *Quaternion.from_euler(Vector3(0, 0, 0))), + "mid360_link": Pose(0.20, -0.20, 0.30, *Quaternion.from_euler(Vector3(0, 0, 0))), }, ) diff --git a/dimos/robot/diy/alfred/effector_high_level.py b/dimos/robot/diy/alfred/effector_high_level.py index 2942c3fcb0..e522bf3c09 100644 --- a/dimos/robot/diy/alfred/effector_high_level.py +++ b/dimos/robot/diy/alfred/effector_high_level.py @@ -85,6 +85,9 @@ async def main(self) -> AsyncGenerator[None, None]: self._client = None logger.info("Alfred high-level connection stopped") + async def handle_cmd_vel(self, msg: Twist) -> None: + await self.move(msg) + @rpc async def move(self, twist: Twist, duration: float = 0.0) -> bool: """Send a Twist as a holonomic velocity command. From 4f433225f024de224ef911afdf30674cfa246e88 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 16:49:21 -0700 Subject: [PATCH 10/22] - --- dimos/robot/test_all_blueprints.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dimos/robot/test_all_blueprints.py b/dimos/robot/test_all_blueprints.py index 9c2816f2e0..876fe70748 100644 --- a/dimos/robot/test_all_blueprints.py +++ b/dimos/robot/test_all_blueprints.py @@ -29,6 +29,7 @@ # These need git LFS, so can't be run on the ubuntu runners. SELF_HOSTED_BLUEPRINTS = frozenset( { + "alfred-nav", "coordinator-basic", "coordinator-cartesian-ik-mock", "coordinator-cartesian-ik-piper", From ac17686f762029f637df1afb420c485434bde1bd Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 20:54:29 -0700 Subject: [PATCH 11/22] - --- dimos/robot/diy/alfred/effector_high_level.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dimos/robot/diy/alfred/effector_high_level.py b/dimos/robot/diy/alfred/effector_high_level.py index e522bf3c09..c32f078f55 100644 --- a/dimos/robot/diy/alfred/effector_high_level.py +++ b/dimos/robot/diy/alfred/effector_high_level.py @@ -74,7 +74,7 @@ async def main(self) -> AsyncGenerator[None, None]: if self._stop_task is not None and not self._stop_task.done(): self._stop_task.cancel() try: - self._send_velocity(0.0, 0.0, 0.0) + await self._send_velocity(0.0, 0.0, 0.0) except Exception as e: logger.error(f"Error stopping Alfred: {e}") if self._client is not None: @@ -107,7 +107,7 @@ async def move(self, twist: Twist, duration: float = 0.0) -> bool: # Negate vy and wz for Alfred's inverted Y-axis frame. # Send before scheduling the watchdog — otherwise it could fire first. - if not self._send_velocity(vx, -vy, -wz): + if not await self._send_velocity(vx, -vy, -wz): return False self._last_velocities = [vx, vy, wz] @@ -121,7 +121,7 @@ async def _auto_stop_movement(self, delay: float) -> None: except asyncio.CancelledError: return try: - self._send_velocity(0.0, 0.0, 0.0) + await self._send_velocity(0.0, 0.0, 0.0) self._last_velocities = [0.0, 0.0, 0.0] except Exception as e: logger.error(f"Auto-stop failed: {e}") @@ -142,7 +142,7 @@ async def move_velocity( await self.move(twist, duration=duration) return f"Started moving with velocity=({x}, {y}, {yaw}) for {duration} seconds" - def _send_velocity(self, vx: float, vy: float, wz: float) -> bool: + async def _send_velocity(self, vx: float, vy: float, wz: float) -> bool: """Send a raw velocity (already in Alfred frame) via Portal RPC.""" if self._client is None: return False @@ -151,7 +151,8 @@ def _send_velocity(self, vx: float, vy: float, wz: float) -> bool: "target_velocity": np.array([vx, vy, wz]), "frame": "local", } - self._client.set_target_velocity(command).result() + future = self._client.set_target_velocity(command) + await asyncio.to_thread(future.result) return True except Exception as e: logger.error(f"Error sending Alfred velocity: {e}") From 98a79a5d34a510c4bce839efd20a2deddc010a97 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 20:56:11 -0700 Subject: [PATCH 12/22] - --- bin/ci-check | 1 + 1 file changed, 1 insertion(+) create mode 120000 bin/ci-check diff --git a/bin/ci-check b/bin/ci-check new file mode 120000 index 0000000000..a220b1e38e --- /dev/null +++ b/bin/ci-check @@ -0,0 +1 @@ +/home/dimensional/dimos/.ignore.enhance/items/bin/ci-check \ No newline at end of file From 86d2cc960727e6c1362ba4c47fa2224bd9214e03 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Fri, 15 May 2026 21:30:15 -0700 Subject: [PATCH 13/22] - --- bin/ci-check | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) mode change 120000 => 100644 bin/ci-check diff --git a/bin/ci-check b/bin/ci-check deleted file mode 120000 index a220b1e38e..0000000000 --- a/bin/ci-check +++ /dev/null @@ -1 +0,0 @@ -/home/dimensional/dimos/.ignore.enhance/items/bin/ci-check \ No newline at end of file diff --git a/bin/ci-check b/bin/ci-check new file mode 100644 index 0000000000..36a8e77ac6 --- /dev/null +++ b/bin/ci-check @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail + +echo "Ensuring pip versions are correct" +no_cuda_flag="" +if ! command -v nvidia-smi &>/dev/null || ! nvidia-smi &>/dev/null; then + no_cuda_flag="--no-extra cuda" +fi +uv sync --frozen --all-extras --no-extra dds --no-extra unitree-dds $no_cuda_flag +uv pip install mypy + +echo "Running pre-commit checks" +time pre-commit run --all-files + +echo "Running mypy checks" +time uv run mypy --python-version 3.10 dimos +time uv run mypy --python-version 3.12 dimos From 037f0c78d215f3460b97f8906a61d5ab93e987df Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 16 May 2026 01:36:49 -0700 Subject: [PATCH 14/22] - --- dimos/navigation/nav_stack/main.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/dimos/navigation/nav_stack/main.py b/dimos/navigation/nav_stack/main.py index 7fdfa45f17..1b05045478 100644 --- a/dimos/navigation/nav_stack/main.py +++ b/dimos/navigation/nav_stack/main.py @@ -197,6 +197,7 @@ def nav_stack_rerun_config( agentic_debug: bool = False, show_registered_scan: bool = False, vis_throttle: float = 1.0, + default_max_hz: int = 60, ) -> dict[str, Any]: """Return a rerun config dict with nav stack visualization defaults. @@ -206,10 +207,6 @@ def nav_stack_rerun_config( Use ``vis_throttle`` (make smaller) if there is crashing related to Rerun/Dimos-Viewer. """ resolved = dict(user_config or {}) - if vis_throttle != 1.0 and "max_hz" in resolved: - resolved["max_hz"] = { - entity: hz * vis_throttle for entity, hz in resolved["max_hz"].items() - } resolved.setdefault("blueprint", _default_rerun_blueprint) resolved.setdefault("pubsubs", [LCM()]) resolved.setdefault("visual_override", {}) @@ -236,6 +233,8 @@ def nav_stack_rerun_config( visual_override.setdefault("world/contour_polygons", _contour_polygons_colors_debug) visual_override.setdefault("world/graph_nodes", _graph_nodes_colors_debug) visual_override.setdefault("world/graph_edges", _graph_edges_colors_debug) + visual_override.setdefault("world/pgo_graph_nodes", _pgo_graph_nodes_colors_debug) + visual_override.setdefault("world/pgo_graph_edges", _pgo_graph_edges_colors_debug) else: visual_override.setdefault("world/way_point", _waypoint_colors) visual_override.setdefault("world/goal", _goal_colors) @@ -251,6 +250,13 @@ def nav_stack_rerun_config( static_entries = dict(resolved["static"]) static_entries.setdefault("world/floor", _static_floor) resolved["static"] = static_entries + # scale/limit rendering (mostly preveting rerun from crashing) + resolved.setdefault("max_hz", {}) + resolved["max_hz"] = { + each_entity: resolved["max_hz"].get(each_entity, default_max_hz) * vis_throttle + for each_entity in visual_override + } + return resolved From ca977fa1707e2e36204a849f970ae233db97c8e1 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 16 May 2026 01:39:24 -0700 Subject: [PATCH 15/22] - --- dimos/navigation/nav_stack/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dimos/navigation/nav_stack/main.py b/dimos/navigation/nav_stack/main.py index 1b05045478..2dd3920e1d 100644 --- a/dimos/navigation/nav_stack/main.py +++ b/dimos/navigation/nav_stack/main.py @@ -233,8 +233,6 @@ def nav_stack_rerun_config( visual_override.setdefault("world/contour_polygons", _contour_polygons_colors_debug) visual_override.setdefault("world/graph_nodes", _graph_nodes_colors_debug) visual_override.setdefault("world/graph_edges", _graph_edges_colors_debug) - visual_override.setdefault("world/pgo_graph_nodes", _pgo_graph_nodes_colors_debug) - visual_override.setdefault("world/pgo_graph_edges", _pgo_graph_edges_colors_debug) else: visual_override.setdefault("world/way_point", _waypoint_colors) visual_override.setdefault("world/goal", _goal_colors) From f5063b81aae8760ec151b3ed89486d24e86bcc19 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 16 May 2026 07:39:40 -0700 Subject: [PATCH 16/22] ci: bump self-hosted-tests timeout from 30 to 45 minutes macOS arm64 runs tests in ~25min then needs ~5min for codecov upload, hitting the 30min ceiling. Adds headroom so the codecov step finishes without the job being cancelled and ci-complete going red. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9f55556fc..f998f324ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,7 +142,7 @@ jobs: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ALIBABA_API_KEY: ${{ secrets.ALIBABA_API_KEY }} - timeout-minutes: 30 + timeout-minutes: 45 strategy: fail-fast: false matrix: From 8d3d7682879bad725440be528d0f4c45a4379cd4 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 16 May 2026 10:38:14 -0700 Subject: [PATCH 17/22] Update dimos/navigation/nav_stack/main.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- dimos/navigation/nav_stack/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dimos/navigation/nav_stack/main.py b/dimos/navigation/nav_stack/main.py index 2dd3920e1d..d40db119ed 100644 --- a/dimos/navigation/nav_stack/main.py +++ b/dimos/navigation/nav_stack/main.py @@ -250,7 +250,7 @@ def nav_stack_rerun_config( resolved["static"] = static_entries # scale/limit rendering (mostly preveting rerun from crashing) resolved.setdefault("max_hz", {}) - resolved["max_hz"] = { + # scale/limit rendering (mostly preventing rerun from crashing) each_entity: resolved["max_hz"].get(each_entity, default_max_hz) * vis_throttle for each_entity in visual_override } From abc78e4b6dcbacc040c4502b32e1ceb8a4267983 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 16 May 2026 10:42:19 -0700 Subject: [PATCH 18/22] fix: address greptile review on nav_stack/main.py max_hz block - Restore missing dict-comprehension assignment target and opening brace (P0: previous code was a SyntaxError, broke import of the whole module). - Iterate over union of visual_override and existing max_hz keys so caller-provided rate limits outside the default entity list survive (P1: previously silently dropped). - Fix typo in comment 'preveting' -> 'preventing' (P2). Addresses greptile comments on PR #2108. --- dimos/navigation/nav_stack/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dimos/navigation/nav_stack/main.py b/dimos/navigation/nav_stack/main.py index d40db119ed..4729233507 100644 --- a/dimos/navigation/nav_stack/main.py +++ b/dimos/navigation/nav_stack/main.py @@ -248,11 +248,11 @@ def nav_stack_rerun_config( static_entries = dict(resolved["static"]) static_entries.setdefault("world/floor", _static_floor) resolved["static"] = static_entries - # scale/limit rendering (mostly preveting rerun from crashing) - resolved.setdefault("max_hz", {}) # scale/limit rendering (mostly preventing rerun from crashing) + resolved.setdefault("max_hz", {}) + resolved["max_hz"] = { each_entity: resolved["max_hz"].get(each_entity, default_max_hz) * vis_throttle - for each_entity in visual_override + for each_entity in set(visual_override) | set(resolved["max_hz"]) } return resolved From 957dc680527a02b9c6673e50993e55b74fbc09fe Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 16 May 2026 12:54:55 -0700 Subject: [PATCH 19/22] review: address greptile on effector_high_level.py:125 --- dimos/robot/diy/alfred/effector_high_level.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dimos/robot/diy/alfred/effector_high_level.py b/dimos/robot/diy/alfred/effector_high_level.py index c32f078f55..e25a106688 100644 --- a/dimos/robot/diy/alfred/effector_high_level.py +++ b/dimos/robot/diy/alfred/effector_high_level.py @@ -121,8 +121,8 @@ async def _auto_stop_movement(self, delay: float) -> None: except asyncio.CancelledError: return try: - await self._send_velocity(0.0, 0.0, 0.0) - self._last_velocities = [0.0, 0.0, 0.0] + if await self._send_velocity(0.0, 0.0, 0.0): + self._last_velocities = [0.0, 0.0, 0.0] except Exception as e: logger.error(f"Auto-stop failed: {e}") From 3f6bee3c82f1f57c50f5998efe9b5cc1aeee7479 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 16 May 2026 13:18:53 -0700 Subject: [PATCH 20/22] review: address greptile on bin/ci-check:1 Make bin/ci-check executable (chmod +x), matching peer scripts in bin/. --- bin/ci-check | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bin/ci-check diff --git a/bin/ci-check b/bin/ci-check old mode 100644 new mode 100755 From 24f649a51b2274cc88f5f4c5f93cf4dda441cdc3 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 16 May 2026 16:12:12 -0700 Subject: [PATCH 21/22] - --- bin/ci-check | 1 - 1 file changed, 1 deletion(-) delete mode 120000 bin/ci-check diff --git a/bin/ci-check b/bin/ci-check deleted file mode 120000 index a220b1e38e..0000000000 --- a/bin/ci-check +++ /dev/null @@ -1 +0,0 @@ -/home/dimensional/dimos/.ignore.enhance/items/bin/ci-check \ No newline at end of file From e2305e06624e992497f98ceaac26d0daa7996829 Mon Sep 17 00:00:00 2001 From: Jeff Hykin Date: Sat, 16 May 2026 16:15:29 -0700 Subject: [PATCH 22/22] - --- dimos/robot/diy/alfred/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dimos/robot/diy/alfred/config.py b/dimos/robot/diy/alfred/config.py index f9645ab2b7..c3410f786b 100644 --- a/dimos/robot/diy/alfred/config.py +++ b/dimos/robot/diy/alfred/config.py @@ -30,8 +30,7 @@ height_clearance=2.0, # meters width_clearance=1.0, internal_odom_offsets={ - # Mid-360 lidar: 0.20 m forward, 0.20 m right of base center, 0.10 m above ground. - # it is mounted at an angle but the livox driver will handle that automatically + # Mid-360 lidar: a bit forward, and a bit to the right of base center, above ground. "mid360_link": Pose(0.20, -0.20, 0.30, *Quaternion.from_euler(Vector3(0, 0, 0))), }, )