Skip to content
Closed
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fe7a7de
spec: openspec init
TomCC7 Jun 4, 2026
76158b2
chore: revert change to doc folder
TomCC7 Jun 8, 2026
35c8b14
Merge branch 'main' into cc/feat/openspec
TomCC7 Jun 8, 2026
12d4346
Merge branch 'main' into cc/feat/openspec
TomCC7 Jun 10, 2026
6cd2fd3
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jun 12, 2026
45f7f73
Merge branch 'main' into cc/feat/openspec
TomCC7 Jun 15, 2026
86a600d
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jun 18, 2026
43fd853
Merge branch 'main' into cc/feat/openspec
TomCC7 Jun 20, 2026
8394a61
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jun 20, 2026
bae46c4
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jun 23, 2026
4cf815e
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jun 30, 2026
2c80dab
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jul 7, 2026
bc381cb
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jul 11, 2026
3a976da
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jul 14, 2026
4e25297
add mattskill
TomCC7 Jul 20, 2026
11f0d7f
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jul 23, 2026
9ffcd58
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jul 23, 2026
f873ddf
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jul 24, 2026
e87e93e
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jul 27, 2026
e689348
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jul 28, 2026
794e585
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Jul 29, 2026
cfa3e3a
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Aug 1, 2026
d221a4f
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Aug 4, 2026
df82a32
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Aug 5, 2026
9cac56d
Merge remote-tracking branch 'origin/main' into cc/feat/openspec
TomCC7 Aug 8, 2026
f379a95
Merge remote-tracking branch 'origin/main' into cc/chore/manip-global…
TomCC7 Sep 8, 2026
1339b27
refactor(manipulation): localize device connection configuration
TomCC7 Sep 8, 2026
2574212
spec: remove
TomCC7 Sep 8, 2026
3a122cb
refactor(control): compose typed hardware connection backends
TomCC7 Sep 8, 2026
871d1f3
refactor(manipulation): simplify local connection configuration
TomCC7 Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions dimos/control/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,16 @@ def _setup_hardware(self, component: HardwareComponent) -> None:
else:
adapter = self._create_adapter(component)

if not adapter.connect():
raise RuntimeError(f"Failed to connect to {component.adapter_type} adapter")

try:
logger.info(
"Connecting hardware",
hardware_id=component.hardware_id,
adapter_type=component.adapter_type,
address=component.address,
)
if not adapter.connect():
raise RuntimeError(f"Failed to connect to {component.adapter_type} adapter")

if component.auto_enable:
activate = getattr(adapter, "activate", None)
if callable(activate):
Expand All @@ -254,7 +260,10 @@ def _setup_hardware(self, component: HardwareComponent) -> None:

self.add_hardware(adapter, component)
except Exception:
adapter.disconnect()
try:
adapter.disconnect()
except Exception:
logger.exception("Hardware cleanup failed", hardware_id=component.hardware_id)
raise

def _create_adapter(self, component: HardwareComponent) -> ManipulatorAdapter:
Expand Down
47 changes: 47 additions & 0 deletions dimos/control/test_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,53 @@ class _EEFTwistCoordinator(ControlCoordinator):


class TestControlCoordinatorLifecycle:
@pytest.mark.parametrize("failure", ["false", "raise", "activation"])
def test_failed_connection_attempt_disconnects_and_preserves_error(
self, make_coordinator, mocker, failure
):
coordinator = make_coordinator()
adapter = mocker.MagicMock(spec=ManipulatorAdapter)
adapter.connect.return_value = failure != "false"
if failure == "raise":
adapter.connect.side_effect = RuntimeError("connection failed")
if failure == "activation":
adapter.activate.return_value = False
adapter.disconnect.side_effect = RuntimeError("cleanup failed")
mocker.patch.object(coordinator, "_create_adapter", return_value=adapter)
add = mocker.patch.object(coordinator, "add_hardware")
component = HardwareComponent(
hardware_id="arm", hardware_type=HardwareType.MANIPULATOR, adapter_type="xarm"
)

with pytest.raises(RuntimeError, match="connect|activate"):
coordinator._setup_hardware(component)

adapter.disconnect.assert_called_once_with()
add.assert_not_called()

def test_later_connection_failure_rolls_back_earlier_hardware(self, make_coordinator, mocker):
components = [
HardwareComponent(
hardware_id=name,
hardware_type=HardwareType.MANIPULATOR,
joints=make_joints(name, 6),
)
for name in ("left", "right")
]
coordinator = make_coordinator(hardware=components)
left = mocker.MagicMock(spec=ManipulatorAdapter)
right = mocker.MagicMock(spec=ManipulatorAdapter)
left.connect.return_value = True
right.connect.return_value = False
mocker.patch.object(coordinator, "_create_adapter", side_effect=[left, right])

with pytest.raises(RuntimeError, match="Failed to connect"):
coordinator._setup_from_config()

left.disconnect.assert_called_once_with()
right.disconnect.assert_called_once_with()
assert coordinator._hardware == {}

def test_start_subscribes_ee_twist_only_for_eef_twist_tasks(self, make_coordinator, mocker):
mocker.patch("dimos.core.module.Module.start")
mocker.patch("dimos.control.coordinator.TickLoop")
Expand Down
4 changes: 0 additions & 4 deletions dimos/core/global_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ class GlobalConfig(BaseSettings):
# Per-device AES-128 key for new Unitree firmware (G1 >=1.5.1, Go2 >=1.1.15, data2=3
# handshake). Fetch: unitree-fetch-aes-key --email YOU --sn <serial>
unitree_aes_128_key: str | None = None
xarm7_ip: str | None = None
xarm6_ip: str | None = None
can_port: str | None = None
device_path: str | None = None # device path for real robot (e.g. /dev/ttyUSB0)
simulation: str = ""
replay: bool = False
replay_db: str = "go2_short"
Expand Down
3 changes: 3 additions & 0 deletions dimos/experimental/world_belief/xarm6_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from dimos.msgs.geometry_msgs.Vector3 import Vector3
from dimos.robot.manipulators.common.blueprints import coordinator, trajectory_task
from dimos.robot.manipulators.xarm.config import make_xarm6_model_config, xarm6_hardware
from dimos.robot.manipulators.xarm.coordinator import XArm6Coordinator
from dimos.visualization.rerun.bridge import RerunBridgeModule

if TYPE_CHECKING:
Expand Down Expand Up @@ -128,6 +129,8 @@ def _rerun_blueprint() -> rrb.Blueprint:
),
McpServer.blueprint(),
coordinator(
cls=XArm6Coordinator,
instance_name="ControlCoordinator",
hardware=[_hw],
tasks=[trajectory_task(_hw)],
),
Expand Down
4 changes: 2 additions & 2 deletions dimos/hardware/manipulators/galaxea_a1z/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def _socketcan_channel_error(channel: str) -> str | None:
except FileNotFoundError:
return (
f"SocketCAN interface {channel!r} does not exist. The HHS adapter must be "
"bound to the Linux gs_usb driver; pass its interface with --can-port."
"bound to the Linux gs_usb driver; pass its interface with --address."
)
except (OSError, ValueError) as exc:
return f"cannot read SocketCAN interface {channel!r}: {exc}"
Expand All @@ -111,7 +111,7 @@ def _socketcan_channel_error(channel: str) -> str | None:
return (
f"SocketCAN interface {channel!r} belongs to kernel driver {driver!r}, not "
f"the HHS adapter driver {_A1Z_SOCKETCAN_DRIVER!r}. Pass the HHS SocketCAN "
"interface with --can-port."
"interface with --address."
)
if not flags & 0x1:
return (
Expand Down
2 changes: 1 addition & 1 deletion dimos/hardware/manipulators/galaxea_a1z/test_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ def test_socketcan_connect_fails_closed_before_sdk_construction(
"mttcan",
"0x1\n",
"SocketCAN interface 'can7' belongs to kernel driver 'mttcan', not the HHS "
"adapter driver 'gs_usb'. Pass the HHS SocketCAN interface with --can-port.",
"adapter driver 'gs_usb'. Pass the HHS SocketCAN interface with --address.",
),
(
"gs_usb",
Expand Down
7 changes: 4 additions & 3 deletions dimos/imitation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ teleop (Quest) ─▶ CollectionRecorder ─▶ session_<robot>_<ts>.db ─▶ d

## 1. Record a session

Run a collection blueprint. Add `--simulation` to drive MuJoCo; omit it for real
hardware (a RealSense + the arm).
Run a collection blueprint. Add `--simulation` to drive MuJoCo. For real
hardware (a RealSense + the arm), omit simulation and supply the arm's `--address`.
Without an address, the arm is mocked; this does not mock the camera.

```bash
# XArm7 in sim
dimos --simulation run learning-collect-quest-xarm7

# Piper on real hardware
dimos run learning-collect-quest-piper
dimos run learning-collect-quest-piper --address can0
```

This brings up teleop, a RealSense (real only), the episode monitor, and the
Expand Down
21 changes: 21 additions & 0 deletions dimos/robot/all_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@


all_modules = {
"a1-z-coordinator": "dimos.robot.manipulators.a1z.coordinator.A1ZCoordinator",
"a1-z-teleop-coordinator": "dimos.robot.manipulators.a1z.coordinator.A1ZTeleopCoordinator",
"a1-z-twist-coordinator": "dimos.robot.manipulators.a1z.coordinator.A1ZTwistCoordinator",
"a750-coordinator": "dimos.robot.manipulators.a750.coordinator.A750Coordinator",
"alfred-high-level": "dimos.robot.diy.alfred.effector_high_level.AlfredHighLevel",
"alfred-mount-tf": "dimos.robot.diy.alfred.mount_tf.AlfredMountTf",
"arm-command-module": "dimos.teleop.hosted.arm_command.ArmCommandModule",
Expand Down Expand Up @@ -210,6 +214,7 @@
"drone-connection-module": "dimos.robot.drone.connection_module.DroneConnectionModule",
"drone-tracking-module": "dimos.robot.drone.drone_tracking_module.DroneTrackingModule",
"dual-open-yam-coordinator": "dimos.robot.manipulators.dual_openyam.blueprints.basic.DualOpenYamCoordinator",
"dual-x-arm-coordinator": "dimos.robot.manipulators.xarm.coordinator.DualXArmCoordinator",
"emitter-module": "dimos.utils.demo_image_encoding.EmitterModule",
"episode-monitor-module": "dimos.imitation.collection.episode_monitor.EpisodeMonitorModule",
"eval-module": "dimos.evals.module.EvalModule",
Expand Down Expand Up @@ -264,6 +269,7 @@
"mid360-pcap-recorder": "dimos.hardware.sensors.lidar.virtual_mid360.recorder.Mid360PcapRecorder",
"mid360-realsense-recorder": "dimos.robot.assembly.mid360_realsense_30.Mid360RealsenseRecorder",
"mid360-realsense-static-tf": "dimos.robot.assembly.mid360_realsense_30.Mid360RealsenseStaticTf",
"mixed-arm-coordinator": "dimos.robot.manipulators.common.mixed_coordinator.MixedArmCoordinator",
"mls-planner-native": "dimos.navigation.nav_3d.mls_planner.mls_planner_native.MLSPlannerNative",
"mock-b1-connection-module": "dimos.robot.unitree.b1.connection.MockB1ConnectionModule",
"module-a": "dimos.robot.unitree.demo_error_on_name_conflicts.ModuleA",
Expand All @@ -279,6 +285,9 @@
"observe-skill": "dimos.agents.skills.observe_skill.ObserveSkill",
"odometry-hist": "dimos.mapping.odometry_hist.OdometryHist",
"open-arm-teleop-coordinator": "dimos.robot.manipulators.openarm.blueprints.teleop.OpenArmTeleopCoordinator",
"open-yam-coordinator": "dimos.robot.manipulators.openyam.coordinator.OpenYamCoordinator",
"open-yam-teleop-coordinator": "dimos.robot.manipulators.openyam.coordinator.OpenYamTeleopCoordinator",
"open-yam-twist-coordinator": "dimos.robot.manipulators.openyam.coordinator.OpenYamTwistCoordinator",
"osm-skill": "dimos.agents.skills.osm.OsmSkill",
"path-following-coordinator": "dimos.control.path_following_coordinator.PathFollowingCoordinator",
"patrolling-module": "dimos.navigation.patrolling.module.PatrollingModule",
Expand All @@ -287,6 +296,10 @@
"person-tracker": "dimos.perception.detection.person_tracker.PersonTracker",
"phone-teleop-module": "dimos.teleop.phone.phone_teleop_module.PhoneTeleopModule",
"pick-and-place-module": "dimos.manipulation.pick_and_place_module.PickAndPlaceModule",
"piper-coordinator": "dimos.robot.manipulators.piper.coordinator.PiperCoordinator",
"piper-pose-coordinator": "dimos.robot.manipulators.piper.coordinator.PiperPoseCoordinator",
"piper-teleop-coordinator": "dimos.robot.manipulators.piper.coordinator.PiperTeleopCoordinator",
"piper-twist-coordinator": "dimos.robot.manipulators.piper.coordinator.PiperTwistCoordinator",
"point-cloud-self-filter": "dimos.manipulation.planning.utils.point_cloud_self_filter.PointCloudSelfFilter",
"point-lio": "dimos.hardware.sensors.lidar.pointlio.module.PointLio",
"pointlio-recorder": "dimos.hardware.sensors.lidar.pointlio.recorder.PointlioRecorder",
Expand All @@ -308,6 +321,7 @@
"security-module": "dimos.experimental.security_demo.security_module.SecurityModule",
"semantic-search": "dimos.memory.module.SemanticSearch",
"simple-phone-teleop": "dimos.teleop.phone.phone_extensions.SimplePhoneTeleop",
"single-arm-coordinator": "dimos.robot.manipulators.common.connection.SingleArmCoordinator",
"spatial-memory": "dimos.perception.experimental.spatial_perception.SpatialMemory",
"speak-skill": "dimos.agents.skills.speak_skill.SpeakSkill",
"spot-high-level": "dimos.experimental.robot.bosdyn.spot.effectors.high_level.SpotHighLevel",
Expand All @@ -331,5 +345,12 @@
"world-belief-module": "dimos.experimental.world_belief.worldbelief_module.WorldBeliefModule",
"world-belief-recorder": "dimos.experimental.world_belief.worldbelief_recorder.WorldBeliefRecorder",
"wrist-camera": "dimos.teleop.hosted.blueprints.cloudflare.WristCamera",
"x-arm6-coordinator": "dimos.robot.manipulators.xarm.coordinator.XArm6Coordinator",
"x-arm6-hardware-coordinator": "dimos.robot.manipulators.xarm.coordinator.XArm6HardwareCoordinator",
"x-arm6-teleop-coordinator": "dimos.robot.manipulators.xarm.coordinator.XArm6TeleopCoordinator",
"x-arm6-twist-coordinator": "dimos.robot.manipulators.xarm.coordinator.XArm6TwistCoordinator",
"x-arm7-coordinator": "dimos.robot.manipulators.xarm.coordinator.XArm7Coordinator",
"x-arm7-teleop-coordinator": "dimos.robot.manipulators.xarm.coordinator.XArm7TeleopCoordinator",
"x-arm7-twist-coordinator": "dimos.robot.manipulators.xarm.coordinator.XArm7TwistCoordinator",
"zed-camera": "dimos.hardware.sensors.camera.zed.camera.ZEDCamera",
}
8 changes: 6 additions & 2 deletions dimos/robot/manipulators/a1z/blueprints/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@

from __future__ import annotations

from dimos.control.coordinator import ControlCoordinator, TaskConfig
from dimos.control.coordinator import TaskConfig
from dimos.core.coordination.blueprints import autoconnect
from dimos.robot.manipulators.a1z.config import (
A1Z_G1Z_MODEL_PATH,
a1z_hardware,
make_a1z_model_config,
)
from dimos.robot.manipulators.a1z.coordinator import A1ZCoordinator
from dimos.robot.manipulators.common.blueprints import coordinator, planner, trajectory_task

_a1z_planner_hw = a1z_hardware("arm")
Expand All @@ -40,6 +41,8 @@ def _gripper_task() -> TaskConfig:
a1z_planner_coordinator = autoconnect(
planner(model=make_a1z_model_config()),
coordinator(
cls=A1ZCoordinator,
instance_name="ControlCoordinator",
hardware=[_a1z_planner_hw],
tasks=[trajectory_task(_a1z_planner_hw), _gripper_task()],
),
Expand All @@ -50,7 +53,8 @@ def _gripper_task() -> TaskConfig:
dynamics_urdf_path=A1Z_G1Z_MODEL_PATH,
)

coordinator_a1z = ControlCoordinator.blueprint(
coordinator_a1z = A1ZCoordinator.blueprint(
instance_name="ControlCoordinator",
hardware=[_coordinator_a1z_hw],
tasks=[trajectory_task(_coordinator_a1z_hw), _gripper_task()],
)
9 changes: 3 additions & 6 deletions dimos/robot/manipulators/a1z/blueprints/teleop.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,26 @@
from __future__ import annotations

from dimos.control.coordinator import TaskConfig
from dimos.control.teleop_coordinator import TeleopControlCoordinator
from dimos.core.coordination.blueprints import autoconnect
from dimos.manipulation.manipulation_module import ManipulationModule
from dimos.robot.manipulators.a1z.config import (
a1z_hardware,
make_a1z_model_config,
)
from dimos.robot.manipulators.a1z.coordinator import A1ZTeleopCoordinator, A1ZTwistCoordinator
from dimos.robot.manipulators.common.blueprints import (
eef_twist_task,
teleop_ik_task,
trajectory_task,
)
from dimos.robot.manipulators.common.coordinators import (
ArmTwistCoordinator,
)
from dimos.teleop.keyboard.keyboard_teleop_module import KeyboardTeleopModule

_a1z_keyboard_hw = a1z_hardware("arm")
_a1z_model = make_a1z_model_config()

keyboard_teleop_a1z = autoconnect(
KeyboardTeleopModule.blueprint(),
ArmTwistCoordinator.blueprint(
A1ZTwistCoordinator.blueprint(
instance_name="ControlCoordinator",
hardware=[_a1z_keyboard_hw],
tasks=[
Expand Down Expand Up @@ -68,7 +65,7 @@
_a1z_quest_model = make_a1z_model_config()

coordinator_teleop_a1z = autoconnect(
TeleopControlCoordinator.blueprint(
A1ZTeleopCoordinator.blueprint(
instance_name="ControlCoordinator",
hardware=[_a1z_quest_hw],
tasks=[
Expand Down
17 changes: 4 additions & 13 deletions dimos/robot/manipulators/a1z/blueprints/test_teleop.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

from dimos.control.coordinator import ControlCoordinator, TaskConfig
from dimos.core.coordination.blueprints import Blueprint
from dimos.core.global_config import global_config
from dimos.robot.manipulators.a1z.blueprints.basic import a1z_planner_coordinator
from dimos.robot.manipulators.a1z.blueprints.teleop import (
coordinator_teleop_a1z,
Expand Down Expand Up @@ -77,11 +76,8 @@ def test_quest_left_controller_routes_to_a1z_teleop() -> None:
}


def test_a1z_hardware_uses_mock_adapter_in_simulation(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(global_config, "can_port", "a1zcan")
monkeypatch.setattr(global_config, "simulation", "mujoco")

hardware = a1z_hardware("arm")
def test_a1z_hardware_uses_mock_adapter_in_simulation() -> None:
hardware = a1z_hardware("arm", address="a1zcan", simulation="mujoco")

assert hardware.adapter_type == "mock"
assert hardware.address is None
Expand All @@ -90,13 +86,8 @@ def test_a1z_hardware_uses_mock_adapter_in_simulation(monkeypatch: pytest.Monkey
assert hardware.joints[-1] == "arm/gripper"


def test_a1z_hardware_uses_real_adapter_when_can_port_is_selected(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(global_config, "can_port", "a1zcan")
monkeypatch.setattr(global_config, "simulation", "")

hardware = a1z_hardware("arm")
def test_a1z_hardware_uses_real_adapter_when_address_is_selected() -> None:
hardware = a1z_hardware("arm", address="a1zcan")

assert hardware.adapter_type == "galaxea_a1z"
assert hardware.address == "a1zcan"
Expand Down
Loading
Loading