Skip to content

feat: in-house robot asset manager#2505

Open
TomCC7 wants to merge 22 commits into
mainfrom
cc/chore/robot-description
Open

feat: in-house robot asset manager#2505
TomCC7 wants to merge 22 commits into
mainfrom
cc/chore/robot-description

Conversation

@TomCC7

@TomCC7 TomCC7 commented Jun 16, 2026

Copy link
Copy Markdown
Member

Problem

DimOS currently keeps several manipulation robot model bundles in Git LFS. That copies upstream robot description assets into this repo, makes updates manual, and makes CI brittle on runners that do not have those LFS objects available.

robot_descriptions.py solves a related problem, but it is not the right main asset layer for DimOS:

  • it is a curated registry of pre-registered description modules, not a direct way to point DimOS at any upstream robot description repository;
  • Piper and A-750 are not covered, and OpenArm still has DimOS-local model changes;
  • DimOS catalogs need to choose exact repo refs, package roots, Xacro args, and repo-relative files that match the existing planning/control stack;
  • relying on an external registry would make routine robot source changes depend on upstream registry changes.

This PR keeps one Git-backed robot asset supply path for built-in and user-supplied robots. pip install dimos installs code only; repos are resolved lazily when a concrete model path is used.

Solution

Add a small in-house robot description source layer:

  • add GitAssetCache for fresh-when-safe Git checkouts under the platform user cache directory, in dimos/robot_assets;
  • preserve local edits and local commits in cached checkouts by warning and skipping upstream updates;
  • add RobotDescriptionSource, a lazy path-like source handle for an upstream repo/ref;
  • let catalogs form paths directly from the source root, for example xarm_repo / "xarm_description" / "urdf" / "xarm_device.urdf.xacro";
  • remove the earlier central registry/artifact-role design so custom robot adapters can create their own source handles without registration;
  • migrate xArm, Piper, and A-750 runtime model paths and package roots to Git-backed source handles where upstream sources are suitable;
  • keep LFS for assets that still need DimOS-local/generated variants, such as FK-only/no-gripper models, sim assets, and OpenArm's locally modified models;
  • keep universal URDF/Xacro rendering and package URI handling in dimos/robot/assets, while Drake-specific cleanup stays in Drake utilities.

The asset code is self-contained under dimos/robot/assets and avoids an __init__.py to match DimOS import policy.

How to Test

Manual check for a human reviewer:

uv run --extra manipulation dimos run xarm7-planner-coordinator

Open the Drake Meshcat URL printed in the logs. The xArm7 model should load and visualize correctly. The first run may clone xarm_ros2 into the platform user cache under dimos/robot_assets; derived URDFs should be written under dimos/robot_assets/derived.

For daemon mode:

uv run --extra manipulation dimos run xarm7-planner-coordinator --daemon
uv run --extra manipulation dimos log -f

Contributor License Agreement

  • I have read and approved the CLA.

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
1905 1 1904 159
View the top 1 failed test(s) by shortest run time
::dimos.manipulation.test_manipulation_module
Stack Traces | 0s run time
dimos/manipulation/test_manipulation_module.py:29: in <module>
    from dimos.manipulation.blueprints import _XARM_MODEL_PATH, _XARM_PACKAGE_PATHS
        MagicMock  = <class 'unittest.mock.MagicMock'>
        __builtins__ = <builtins>
        __cached__ = '.../manipulation/__pycache__/test_manipulation_module.cpython-312.pyc'
        __doc__    = '\nIntegration tests for ManipulationModule.\n\nThese tests verify the full planning stack with Drake backend.\nThey require Drake to be installed and will be skipped otherwise.\n'
        __file__   = '.../dimos/manipulation/test_manipulation_module.py'
        __loader__ = <_pytest.assertion.rewrite.AssertionRewritingHook object at 0xffc9f6b7dac0>
        __name__   = 'dimos.manipulation.test_manipulation_module'
        __package__ = 'dimos.manipulation'
        __spec__   = ModuleSpec(name='dimos.manipulation.test_manipulation_module', loader=<_pytest.assertion.rewrite.AssertionRewritingHook object at 0xffc9f6b7dac0>, origin='.../dimos/manipulation/test_manipulation_module.py')
        annotations = _Feature((3, 7, 0, 'beta', 1), None, 16777216)
        importlib  = <module 'importlib' from '.../usr/lib/python3.12/importlib/__init__.py'>
        pytest     = <module 'pytest' from '.../dimos/dimos/.venv/lib/python3.12.../site-packages/pytest/__init__.py'>
dimos/manipulation/blueprints.py:368: in <module>
    address=str(XARM7_SIM_PATH),
        Any        = typing.Any
        ControlCoordinator = <class 'dimos.control.coordinator.ControlCoordinator'>
        ManipulationModule = <class 'dimos.manipulation.manipulation_module.ManipulationModule'>
        McpClient  = <class 'dimos.agents.mcp.mcp_client.McpClient'>
        McpServer  = <class 'dimos.agents.mcp.mcp_server.McpServer'>
        MujocoSimModule = <class 'dimos.simulation.engines.mujoco_sim_module.MujocoSimModule'>
        ObjectSceneRegistrationModule = <class 'dimos.perception.object_scene_registration.ObjectSceneRegistrationModule'>
        Path       = <class 'pathlib.Path'>
        PickAndPlaceModule = <class 'dimos.manipulation.pick_and_place_module.PickAndPlaceModule'>
        PoseStamped = <class 'dimos.msgs.geometry_msgs.PoseStamped.PoseStamped'>
        Quaternion = <class 'dimos.msgs.geometry_msgs.Quaternion.Quaternion'>
        RealSenseCamera = <class 'dimos.hardware.sensors.camera.realsense.camera.RealSenseCamera'>
        RerunBridgeModule = <class 'dimos.visualization.rerun.bridge.RerunBridgeModule'>
        RobotDescriptionSource = <class 'dimos.robot.assets.source.RobotDescriptionSource'>
        RobotModelConfig = <class 'dimos.manipulation.planning.spec.config.RobotModelConfig'>
        TaskConfig = <class 'dimos.control.coordinator.TaskConfig'>
        Transform  = <class 'dimos.msgs.geometry_msgs.Transform.Transform'>
        Vector3    = <class 'dimos.msgs.geometry_msgs.Vector3.Vector3'>
        XARM7_SIM_PATH = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/xarm7.tar.gz after 3 attempts: Command...ude', 'data/.lfs/xarm7.tar.gz']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0xffc9268d5950>
        XARM_GRIPPER_COLLISION_EXCLUSIONS = [('right_inner_knuckle', 'right_outer_knuckle'), ('left_inner_knuckle', 'left_outer_knuckle'), ('right_inner_knuckle',...ft_inner_knuckle', 'left_finger'), ('left_finger', 'right_finger'), ('left_outer_knuckle', 'right_outer_knuckle'), ...]
        XARM_ROS2_REPO = 'https://github.com/xArm-Developer/xarm_ros2'
        _BASE_MANIPULATION_AGENT_SYSTEM_PROMPT = 'You are a robotic manipulation assistant controlling an xArm7 robot arm.\n\nAvailable skills:\n- get_robot_state: Get...e robot to near-origin.\n\nERROR RECOVERY: If a motion fails or the state becomes FAULT, call reset before retrying.\n'
        _MANIPULATION_AGENT_SYSTEM_PROMPT = 'You are a robotic manipulation assistant controlling an xArm7 robot arm with an eye-in-hand RealSense camera and a gr...ecovery\nIf planning fails with COLLISION_AT_START: call **clear_perception_obstacles**, then **reset**, then retry.\n'
        _XARM_MODEL_PATH = PosixPath('....../home/runner/.cache.../xarm_description/urdf/xarm_device.urdf.xacro')
        _XARM_PACKAGE_PATHS = {'xarm_description': PosixPath('....../home/runner/.cache.../d010b37685bff17b/xarm_ros2/xarm_description')}
        _XARM_PERCEPTION_CAMERA_TRANSFORM = Transform(translation=Vector([   0.066937   -0.030956   0.0069148]), rotation=Quaternion(0.705134, 0.005357, 0.708976, -0.010522))
        _XARM_REPO = RobotDescriptionSource(url='https://github.com/xArm-Developer/xarm_ros2', ref='humble')
        __annotations__ = {'XARM_GRIPPER_COLLISION_EXCLUSIONS': list[tuple[str, str]], '_XARM_PACKAGE_PATHS': dict[str, pathlib.Path]}
        __builtins__ = <builtins>
        __cached__ = '.../manipulation/__pycache__/blueprints.cpython-312.pyc'
        __doc__    = '\nManipulation blueprints.\n\nQuick start:\n    # 1. Verify manipulation deps load correctly (standalone, no hardware...\n    dimos run xarm7-planner-coordinator\n    python -i -m dimos.manipulation.planning.examples.manipulation_client\n'
        __file__   = '.../dimos/manipulation/blueprints.py'
        __loader__ = <_frozen_importlib_external.SourceFileLoader object at 0xffc926aab650>
        __name__   = 'dimos.manipulation.blueprints'
        __package__ = 'dimos.manipulation'
        __spec__   = ModuleSpec(name='dimos.manipulation.blueprints', loader=<_frozen_importlib_external.SourceFileLoader object at 0xffc926aab650>, origin='.../dimos/manipulation/blueprints.py')
        _base_pose = <function _base_pose at 0xffc926b82980>
        _coordinator_joint_mapping = <function _coordinator_joint_mapping at 0xffc92678f9c0>
        _make_xarm6_model_config = <function _make_xarm6_model_config at 0xffc9267faf20>
        _make_xarm7_model_config = <function _make_xarm7_model_config at 0xffc9267fafc0>
        _make_xarm_model_config = <function _make_xarm_model_config at 0xffc9267fae80>
        _xarm7_hw  = HardwareComponent(hardware_id='arm', hardware_type=<HardwareType.MANIPULATOR: 'manipulator'>, joints=['arm/joint1', 'a...'mock', address=None, auto_enable=True, gripper_joints=['arm/gripper'], domain_id=0, adapter_kwargs={}, wb_config=None)
        _xarm7_sim_home = [0.0, 0.0, 0.0, 0.0, 0.0, -0.7, ...]
        autoconnect = <function autoconnect at 0xffc9c4ff28e0>
        dual_xarm6_planner = Blueprint(blueprints=(BlueprintAtom(kwargs={'robots': [RobotModelConfig(rpc_transport=<class 'dimos.protocol.rpc.pubsu...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
        global_config = GlobalConfig(robot_ip=None, robot_ips=None, unitree_aes_128_key=None, xarm7_ip=None, xarm6_ip=None, can_port=None, dev...e, obstacle_avoidance=True, detection_model='moondream', listen_host='127.0.0.1', dimsim_scene='apt', dimsim_port=8090)
        manipulator = <function manipulator at 0xffc926a19da0>
        math       = <module 'math' (built-in)>
        xarm6_planner_only = Blueprint(blueprints=(BlueprintAtom(kwargs={'robots': [RobotModelConfig(rpc_transport=<class 'dimos.protocol.rpc.pubsu...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
        xarm7_planner_coordinator = Blueprint(blueprints=(BlueprintAtom(kwargs={'robots': [RobotModelConfig(rpc_transport=<class 'dimos.protocol.rpc.pubsu...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
        xarm7_planner_coordinator_agent = Blueprint(blueprints=(BlueprintAtom(kwargs={'robots': [RobotModelConfig(rpc_transport=<class 'dimos.protocol.rpc.pubsu...lobal_config_overrides=mappingproxy({}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
        xarm_perception = Blueprint(blueprints=(BlueprintAtom(kwargs={'robots': [RobotModelConfig(rpc_transport=<class 'dimos.protocol.rpc.pubsu...verrides=mappingproxy({'n_workers': 4}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
        xarm_perception_agent = Blueprint(blueprints=(BlueprintAtom(kwargs={'robots': [RobotModelConfig(rpc_transport=<class 'dimos.protocol.rpc.pubsu...verrides=mappingproxy({'n_workers': 4}), remapping_map=mappingproxy({}), requirement_checks=(), configurator_checks=())
dimos/utils/data.py:369: in __str__
    return str(self._ensure_downloaded())
        self       = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/xarm7.tar.gz after 3 attempts: Command...ude', 'data/.lfs/xarm7.tar.gz']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0xffc9268d5950>
dimos/utils/data.py:347: in _ensure_downloaded
    cache = get_data(filename)
        cache      = None
        filename   = 'xarm7/scene.xml'
        self       = <[RuntimeError("Failed to pull LFS file .../dimos/data/.lfs/xarm7.tar.gz after 3 attempts: Command...ude', 'data/.lfs/xarm7.tar.gz']' returned non-zero exit status 1.") raised in repr()] LfsPath object at 0xffc9268d5950>
dimos/utils/data.py:304: in get_data
    archive_path = _decompress_archive(_pull_lfs_archive(archive_name))
        archive_name = 'xarm7'
        data_dir   = PosixPath('.../dimos/dimos/data')
        file_path  = PosixPath('.../dimos/dimos/data/xarm7/scene.xml')
        name       = 'xarm7/scene.xml'
        nested_path = PosixPath('scene.xml')
        path_parts = ('xarm7', 'scene.xml')
dimos/utils/data.py:248: in _pull_lfs_archive
    _lfs_pull(file_path, repo_root)
        file_path  = PosixPath('.../dimos/data/.lfs/xarm7.tar.gz')
        filename   = 'xarm7'
        repo_root  = PosixPath('.../work/dimos/dimos')
dimos/utils/data.py:216: in _lfs_pull
    raise RuntimeError(
E   RuntimeError: Failed to pull LFS file .../dimos/data/.lfs/xarm7.tar.gz after 3 attempts: Command '['git', 'lfs', 'pull', '--include', 'data/.lfs/xarm7.tar.gz']' returned non-zero exit status 1.
        attempt    = 3
        env        = {'ACCEPT_EULA': 'Y', 'ACTIONS_ID_TOKEN_REQUEST_TOKEN': 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjM4ODI2YjE3LTZhMzAtNWY5Yi1iMTY5LT...-version=2.0', 'ACTIONS_ORCHESTRATION_ID': '3d17571a-dd54-41bb-8719-9b0ea0d8177b.tests.ubuntu-24_04-arm_3_14_fal', ...}
        file_path  = PosixPath('.../dimos/data/.lfs/xarm7.tar.gz')
        last_err   = CalledProcessError(1, ['git', 'lfs', 'pull', '--include', 'data/.lfs/xarm7.tar.gz'])
        relative_path = PosixPath('data/.lfs/xarm7.tar.gz')
        repo_root  = PosixPath('.../work/dimos/dimos')
        retries    = 2

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@TomCC7 TomCC7 changed the title WIP: in-house robot description loader feat: in-house robot description loader Jun 16, 2026
@TomCC7 TomCC7 marked this pull request as ready for review June 16, 2026 20:08
@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a Git-backed robot asset manager (GitAssetCache, RobotDescriptionSource, RobotDescriptionPath) that replaces LFS bundles for upstream robot descriptions, resolving repos lazily into a platform user-cache directory. xArm, Piper, and A750 runtime model paths are migrated to the new source handles, and the shared URDF rendering pipeline is refactored so Drake-specific cleanup is a thin second layer on top of the universal render_urdf stage.

  • New dimos/robot/assets/ module: git_cache.py handles clone/fetch/dirty-detection with FileLock serialization; source.py provides lazy Path-compatible handles that defer checkout until a path is first observed; processing.py implements a two-level render cache (universal rendered URDF, then Drake-specific) with content-hash–based cache keys.
  • Blueprint migration: xArm, Piper, and A750 _MODEL_PATH and _PACKAGE_PATHS now resolve from upstream GitHub repos; LFS is kept only for FK-only no-gripper models, sim assets, and locally-modified models (OpenArm).
  • mesh_utils.prepare_urdf_for_drake delegates xacro expansion and package:// URI rewriting to render_urdf, reducing Drake-specific logic to transmission-block stripping and optional mesh conversion.

Confidence Score: 4/5

Safe to merge pending one clarification: verify the upstream a750_description URDF does not contain package://a750_gazebo/... URIs before removing that alias from _A750_PACKAGE_PATHS.

The core asset manager logic is well-structured and the vast majority of the migration is correct. The one concern is the removal of the a750_gazebo package alias in _A750_PACKAGE_PATHS — the old code deliberately mapped both names to the same directory, which implies the URDF (or its xacro includes) references package://a750_gazebo/... URIs. If the upstream repo retains those references, Drake will receive an unresolvable URI and silently drop or fail to load the affected meshes.

dimos/robot/manipulators/a750/blueprints.py — verify the a750_gazebo package alias removal is safe against the upstream URDF contents.

Important Files Changed

Filename Overview
dimos/robot/assets/git_cache.py New GitAssetCache: handles clone-on-demand, fetch-on-update, and dirty-skip. Logic is clean; _is_dirty correctly delegates to GitPython; thread safety is handled via FileLock per (url, ref) pair.
dimos/robot/assets/source.py New RobotDescriptionSource / RobotDescriptionPath: lazy Path subclass using getattribute override; correctly defers checkout until path is observed. The type(Path()) subclassing is unusual but functional.
dimos/robot/assets/processing.py New universal URDF render cache. Cache key now includes full directory content hashes (correctness improvement vs prior mtime-only approach), but _directory_fingerprint reads all binary mesh files on every miss — potentially slow for large packages.
dimos/robot/manipulators/a750/blueprints.py Migrated A750 to upstream git source. The a750_gazebo package alias present in the old _A750_PACKAGE_PATHS was dropped without explanation; if the upstream URDF contains package://a750_gazebo/... URIs, Drake model loading will silently fail.
dimos/robot/manipulators/piper/blueprints.py Migrated Piper to upstream git source. Package path mapping correctly uses _PIPER_REPO.parent to match upstream agx_arm_description URI structure; piper_gazebo alias removal is safe given the new URI layout.
dimos/robot/manipulators/xarm/blueprints.py Migrated xArm to upstream xarm_ros2 humble branch. Package path mapping is straightforward and correct.
dimos/control/blueprints/_hardware.py PIPER_FK_MODEL changed from LFS-backed Mujoco no-gripper XML to upstream URDF. Other FK models (xArm, A750) retain their LFS no-gripper variants; the Piper switch is intentional since upstream piper_description.urdf is arm-only.
dimos/manipulation/planning/utils/mesh_utils.py Drake URDF prep refactored to use the new render_urdf layer. Xacro expansion and package URI rewriting now happen in the shared processing module; Drake-specific cache only handles transmission-stripping and optional mesh conversion.

Reviews (5): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

Comment thread dimos/robot/assets/processing.py
Comment thread dimos/robot/assets/processing.py Outdated
Comment thread dimos/robot/assets/processing.py Outdated
TomCC7 and others added 2 commits June 16, 2026 13:18
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
@TomCC7 TomCC7 changed the title feat: in-house robot description loader feat: in-house robot asset manager Jun 16, 2026
Comment thread dimos/robot/assets/git_cache.py
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Jun 16, 2026
@TomCC7 TomCC7 marked this pull request as draft June 16, 2026 22:10
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Jun 16, 2026
Comment thread dimos/robot/assets/declarations.py Outdated
Comment thread dimos/robot/assets/git_cache.py Outdated
Comment thread dimos/robot/assets/git_cache.py Outdated
Comment thread dimos/robot/assets/manager.py Outdated
@TomCC7 TomCC7 marked this pull request as ready for review June 20, 2026 02:41
@TomCC7 TomCC7 marked this pull request as draft June 20, 2026 02:42
@TomCC7 TomCC7 marked this pull request as ready for review June 20, 2026 03:24
@github-actions github-actions Bot added the ready-to-merge Required CI checks have passed on this PR label Jun 20, 2026
@github-actions github-actions Bot removed the ready-to-merge Required CI checks have passed on this PR label Jun 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant