Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 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
d92a78b
spec: planning group
TomCC7 Jun 13, 2026
1730a5e
feat: add manipulation planning groups
TomCC7 Jun 17, 2026
2c80bf5
chore: merge main and adapt pink ik
TomCC7 Jun 17, 2026
3744ec7
fix: address planning group review feedback
TomCC7 Jun 18, 2026
0a403de
feat: multi-group rrt
TomCC7 Jun 18, 2026
6b36a70
fix: address remaining planning group review
TomCC7 Jun 18, 2026
31731b1
feat: dual xarm example
TomCC7 Jun 18, 2026
fe3957f
refactor: simplify manipulator joint naming
TomCC7 Jun 19, 2026
702ad74
refactor: tighten manipulation joint namespaces
TomCC7 Jun 19, 2026
d370174
refactor: align manipulation planning group APIs
TomCC7 Jun 19, 2026
664e6ea
Merge branch 'main' into cc/spec/movegroup
TomCC7 Jun 19, 2026
b3470bd
Apply suggestions from code review
TomCC7 Jun 19, 2026
a3b8459
docs: note pose group tf follow-up
TomCC7 Jun 19, 2026
1671ddb
refactor: split selected joint state collection
TomCC7 Jun 19, 2026
288fca5
refactor: begin planning by group selection
TomCC7 Jun 19, 2026
96f3fcc
refactor: type drake pose transform
TomCC7 Jun 19, 2026
d5788b2
refactor: name joint target conversion explicitly
TomCC7 Jun 19, 2026
4fcb7ef
refactor: execute generated plans directly
TomCC7 Jun 19, 2026
c07d8da
refactor: remove legacy preview path api
TomCC7 Jun 19, 2026
95548e5
refactor: decouple planning group registry
TomCC7 Jun 19, 2026
54f3bc2
Merge remote-tracking branch 'origin/main' into cc/spec/movegroup
TomCC7 Jun 20, 2026
c6e7782
feat: add viser planning target sets
TomCC7 Jun 20, 2026
a38a90c
fix: stabilize viser ik target solving
TomCC7 Jun 20, 2026
15ba00f
fix: polish viser planning group panel
TomCC7 Jun 20, 2026
04de196
fix: synchronize viser group previews
TomCC7 Jun 20, 2026
7e91f9b
fix: integrate dual xarm6 execution
TomCC7 Jun 20, 2026
4763fb9
fix: polish viser planning controls
TomCC7 Jun 20, 2026
fee4641
Merge origin/main into cc/spec/movegroup
TomCC7 Jun 21, 2026
10e4d31
fix: address quick movegroup review comments
TomCC7 Jun 21, 2026
4f771ce
feat: add composable movegroup planning primitives
TomCC7 Jun 21, 2026
ee904b2
refactor: compose viser planning primitives directly
TomCC7 Jun 21, 2026
0bcce30
refactor: remove viser module access adapter
TomCC7 Jun 22, 2026
8e3d051
refactor: simplify viser panel backend
TomCC7 Jun 22, 2026
6e55e8d
refactor: address movegroup review cleanup
TomCC7 Jun 22, 2026
5345204
refactor: use direct pink imports
TomCC7 Jun 22, 2026
fa35c31
fix: update viser collision target ghost
TomCC7 Jun 22, 2026
a27e822
chore: rpc for list planning group
TomCC7 Jun 22, 2026
a61b46b
openspec remove
TomCC7 Jun 22, 2026
12cf85d
Apply suggestions from code review
TomCC7 Jun 22, 2026
13f3bd0
refactor: remove robot catalog package
TomCC7 Jun 22, 2026
eb78230
fix: address movegroup ci failures
TomCC7 Jun 22, 2026
8a9e1df
test: skip dual blueprint checks without lfs data
TomCC7 Jun 22, 2026
39efbca
fix: align openarm planner coordinator joint state
TomCC7 Jun 22, 2026
84799d1
perf: improve pink ik
TomCC7 Jun 22, 2026
13328cf
Merge remote-tracking branch 'origin/main' into cc/spec/movegroup
TomCC7 Jun 22, 2026
29f52af
fix: address review and ci failures
TomCC7 Jun 22, 2026
ded5457
fix: guard pose planning without planner
TomCC7 Jun 22, 2026
06da1fd
refactor: remove planning identifiers shim
TomCC7 Jun 22, 2026
0478556
chore: group util->joints
TomCC7 Jun 22, 2026
5c573f4
doc: update planning group doc
TomCC7 Jun 22, 2026
e6f361e
chore: split bulky viser panel changes
TomCC7 Jun 22, 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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ dimos restart # stop + re-run with same original args
| `xarm-perception-sim-agent` | xArm | sim | GPT-4o | — | Manipulation + perception + agent, sim |
| `xarm7-planner-coordinator` | xArm7 | real | — | — | Trajectory planner coordinator |
| `teleop-quest-xarm7` | xArm7 | real | — | — | Quest VR teleop |
| `dual-xarm6-planner` | xArm6×2 | real | — | — | Dual-arm motion planner |
| `dual-xarm6-planner-coordinator` | xArm6×2 | real | — | — | Dual-arm motion planner + coordinator |

Run `dimos list` for the full list.

Expand Down
1 change: 0 additions & 1 deletion dimos/control/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ Centralized control system for multi-arm robots with per-joint arbitration.
```bash
# Terminal 1: Run coordinator
dimos run coordinator-mock # Single 7-DOF mock arm
dimos run coordinator-dual-mock # Dual arms (7+6 DOF)
dimos run coordinator-piper-xarm # Real hardware

# Terminal 2: Control via CLI
Expand Down
74 changes: 74 additions & 0 deletions dimos/control/blueprints/test_dual.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright 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.

"""Tests for dual-arm control blueprints."""

from dimos.control.coordinator import ControlCoordinator
from dimos.core.coordination.blueprints import Blueprint
from dimos.manipulation.manipulation_module import ManipulationModule
from dimos.robot.manipulators.xarm.blueprints.basic import coordinator_dual_xarm


def _dual_xarm6_planner() -> Blueprint:
from dimos.robot.manipulators.xarm.blueprints.basic import dual_xarm6_planner

return dual_xarm6_planner


def _coordinator_task_names(blueprint) -> list[str]:
atom = next(atom for atom in blueprint.blueprints if atom.module is ControlCoordinator)
return [task.name for task in atom.kwargs["tasks"]]


def _coordinator_tasks(blueprint):
atom = next(atom for atom in blueprint.blueprints if atom.module is ControlCoordinator)
return atom.kwargs["tasks"]


def _manipulation_robots(blueprint):
atom = next(atom for atom in blueprint.blueprints if atom.module is ManipulationModule)
return atom.kwargs["robots"]


def _manipulation_visualization(blueprint):
atom = next(atom for atom in blueprint.blueprints if atom.module is ManipulationModule)
return atom.kwargs["visualization"]


def test_dual_xarm6_planner_blueprint_has_planner() -> None:
dual_xarm6_planner = _dual_xarm6_planner()
modules = [atom.module for atom in dual_xarm6_planner.blueprints]

assert ManipulationModule in modules


def test_dual_xarm6_planner_uses_meshcat_visualization() -> None:
dual_xarm6_planner = _dual_xarm6_planner()
visualization = _manipulation_visualization(dual_xarm6_planner)

assert visualization == {"backend": "meshcat"}


def test_dual_xarm6_planner_robots_use_global_joint_mapping() -> None:
dual_xarm6_planner = _dual_xarm6_planner()

for robot in _manipulation_robots(dual_xarm6_planner):
assert robot.coordinator_task_name == f"traj_{robot.name}"


def test_dual_coordinator_xarm_task_names_match_manipulation_robot_defaults() -> None:
assert _coordinator_task_names(coordinator_dual_xarm) == [
"traj_left",
"traj_right",
]
17 changes: 17 additions & 0 deletions dimos/control/tasks/trajectory_task/trajectory_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,23 @@ def execute(self, trajectory: JointTrajectory) -> bool:
logger.warning(f"Empty trajectory for {self._name}")
return False

if trajectory.joint_names and trajectory.joint_names != self._joint_names_list:
logger.warning(
f"Joint name mismatch for {self._name}: "
f"expected={self._joint_names_list}, received={trajectory.joint_names}"
)
return False

if not trajectory.joint_names:
expected_joint_count = len(self._joint_names_list)
for point in trajectory.points:
if len(point.positions) != expected_joint_count:
logger.warning(
f"Trajectory point dimension mismatch for {self._name}: "
f"expected={expected_joint_count}, received={len(point.positions)}"
)
return False

# Preempt any active trajectory
if self._state == TrajectoryState.EXECUTING:
logger.info(f"Preempting active trajectory on {self._name}")
Expand Down
45 changes: 45 additions & 0 deletions dimos/control/test_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,29 @@ def test_execute_trajectory(self, trajectory_task, simple_trajectory):
assert trajectory_task.is_active()
assert trajectory_task.get_state() == TrajectoryState.EXECUTING

def test_execute_rejects_mismatched_joint_names(self, trajectory_task):
trajectory = JointTrajectory(
joint_names=["other/joint1", "other/joint2", "other/joint3"],
points=[
TrajectoryPoint(
positions=[0.0, 0.0, 0.0],
velocities=[0.0, 0.0, 0.0],
time_from_start=0.0,
),
TrajectoryPoint(
positions=[1.0, 0.5, 0.25],
velocities=[0.0, 0.0, 0.0],
time_from_start=1.0,
),
],
)

result = trajectory_task.execute(trajectory)

assert result is False
assert not trajectory_task.is_active()
assert trajectory_task.get_state() == TrajectoryState.IDLE

def test_compute_during_trajectory(self, trajectory_task, simple_trajectory, coordinator_state):
t_start = time.perf_counter()
trajectory_task.execute(simple_trajectory)
Expand Down Expand Up @@ -530,6 +553,28 @@ def test_tick_loop_calls_compute(self, mock_adapter):

assert mock_task.compute.call_count > 0

def test_write_all_hardware_logs_rejected_command(self, mocker):
hardware = {"arm": MagicMock()}
hardware["arm"].write_command.return_value = False
log_error = mocker.patch("dimos.control.tick_loop.logger.error")
tick_loop = TickLoop(
tick_rate=100.0,
hardware=hardware,
hardware_lock=threading.Lock(),
tasks={},
task_lock=threading.Lock(),
joint_to_hardware={},
)

tick_loop._write_all_hardware({"arm": ({"arm/joint1": 0.5}, ControlMode.SERVO_POSITION)})

hardware["arm"].write_command.assert_called_once_with(
{"arm/joint1": 0.5}, ControlMode.SERVO_POSITION
)
log_error.assert_called_once_with(
"Hardware %s rejected %d %s command(s)", "arm", 1, "SERVO_POSITION"
)


class TestIntegration:
def test_full_trajectory_execution(self, mock_adapter):
Expand Down
8 changes: 7 additions & 1 deletion dimos/control/tick_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,13 @@ def _write_all_hardware(
for hw_id, (positions, mode) in hw_commands.items():
if hw_id in self._hardware:
try:
self._hardware[hw_id].write_command(positions, mode)
if not self._hardware[hw_id].write_command(positions, mode):
logger.error(
"Hardware %s rejected %d %s command(s)",
hw_id,
len(positions),
mode.name,
)
except Exception as e:
logger.error(f"Failed to write to {hw_id}: {e}")

Expand Down
Loading
Loading