Skip to content

Comments

Feature: Control Coordinator support for mobile base#1277

Merged
spomichter merged 14 commits intodevfrom
feature/mustafa-controlcoordinator-support-for-mobile-base
Feb 23, 2026
Merged

Feature: Control Coordinator support for mobile base#1277
spomichter merged 14 commits intodevfrom
feature/mustafa-controlcoordinator-support-for-mobile-base

Conversation

@mustafab0
Copy link
Contributor

@mustafab0 mustafab0 commented Feb 17, 2026

Problem

The ControlCoordinator only supports joint-level control (manipulator arms). Mobile bases, quadrupeds, and drones take Twist (velocity) commands, so there's no way to control them through the coordinator — blocking mobile manipulation.


Solution

Virtual joints map velocity DOFs into the coordinator's existing joint-centric model (base_vx, base_vy, base_wz) A new twist_command: In[Twist] port converts Twist → virtual joint velocities, feeding the existing routing pipeline.

New TwistBaseAdapter protocol (10 methods, SI units) provides a lightweight hardware abstraction. ConnectedTwistBase inherits ConnectedHardware to keep the tick loop uniform. Includes MockTwistBaseAdapter for testing and FlowBaseAdapter for real holonomic base hardware via Portal RPC.


Breaking Changes

None


How to Test

  1. Run keyboard teleop with mock base: python -m dimos.control.examples.twist_base_keyboard_teleop
  2. Echo /cmd_vel in another terminal: uv run dimos topic echo /cmd_vel

closes DIM-547
closes DIM-546

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 17, 2026

Greptile Summary

Extends the ControlCoordinator to support velocity-commanded mobile platforms (holonomic bases, quadrupeds, drones) alongside existing joint-level manipulator control. Virtual joints (base_vx, base_vy, base_wz) map Twist velocity DOFs into the coordinator's existing joint-centric model, and a new twist_command: In[Twist] port converts incoming Twist messages into virtual joint velocities that feed the existing routing/arbitration pipeline.

  • Adds TwistBaseAdapter protocol (10 methods, SI units) in dimos/hardware/drive_trains/spec.py with auto-discovery registry mirroring the existing manipulator pattern
  • Introduces ConnectedTwistBase subclass that wraps TwistBaseAdapter for the tick loop, with odometry-based position reads and velocity-only writes
  • Provides MockTwistBaseAdapter for testing and FlowBaseAdapter for real holonomic base hardware via Portal RPC
  • Adds blueprint configurations for standalone twist base and mobile manipulation (arm + base) setups
  • ConnectedTwistBase bypasses super().__init__() to avoid the parent's ManipulatorAdapter type check — this works but is fragile if the parent class evolves
  • Lock ordering (hardware_locktask_lock) in _on_twist_command is consistent with existing patterns, no deadlock risk

Confidence Score: 4/5

  • This PR is safe to merge — the new code cleanly integrates with existing patterns and has no runtime-breaking issues.
  • The architecture is sound: virtual joints mapping Twist DOFs into the existing joint-centric pipeline is elegant and non-breaking. Lock ordering is consistent. The main concern is the fragile inheritance pattern in ConnectedTwistBase (bypassing super().__init__()), but this is a maintainability issue rather than a correctness bug. Thread safety, type checking, and the adapter registry pattern are all well-handled.
  • dimos/control/hardware_interface.pyConnectedTwistBase inheritance bypasses super().__init__(), which could cause issues if ConnectedHardware evolves.

Important Files Changed

Filename Overview
dimos/control/coordinator.py Core changes: adds twist_command input port, _on_twist_command handler that maps Twist fields to virtual joints, twist base adapter creation, and ConnectedTwistBase branching in add_hardware. Lock ordering (hardware_lock → task_lock) is consistent with existing patterns. Gripper RPCs correctly guard against twist base hardware.
dimos/control/hardware_interface.py New ConnectedTwistBase subclass that bypasses parent __init__ to avoid ManipulatorAdapter type check. Overrides read_state (odometry-based) and write_command (velocity-only). Skipping super().__init__() is fragile — future parent changes may not propagate.
dimos/control/components.py Adds TWIST_SUFFIX_MAP, make_twist_base_joints(), and HardwareType.BASE enum value. Clean, validated mapping from DOF suffixes to Twist message fields.
dimos/hardware/drive_trains/spec.py New TwistBaseAdapter runtime-checkable Protocol with 10 methods covering connection, info, state reading, control, and enable/disable. Clean, minimal interface using SI units.
dimos/hardware/drive_trains/registry.py New TwistBaseAdapterRegistry with auto-discovery from subpackages. Mirrors the existing AdapterRegistry pattern from dimos/hardware/manipulators/registry.py. Discovery catches ImportError gracefully.
dimos/hardware/drive_trains/flowbase/adapter.py FlowBase adapter for holonomic base via Portal RPC. Includes frame convention negation (vy, wz). Thread-safe with _lock. Top-level import numpy is fine since registry discovery catches ImportError.
dimos/control/blueprints.py Adds two new blueprint configs: coordinator_mock_twist_base (standalone base) and coordinator_mobile_manip_mock (arm + base). Follows existing blueprint patterns consistently.

Sequence Diagram

sequenceDiagram
    participant KB as KeyboardTeleop
    participant LCM as LCM /cmd_vel
    participant CC as ControlCoordinator
    participant VJ as Virtual Joints
    participant TL as TickLoop
    participant CTB as ConnectedTwistBase
    participant Adapter as TwistBaseAdapter

    KB->>LCM: Twist(linear, angular)
    LCM->>CC: twist_command subscription
    CC->>CC: _on_twist_command()
    Note over CC: Map Twist fields to virtual joints<br/>base_vx ← linear.x<br/>base_vy ← linear.y<br/>base_wz ← angular.z
    CC->>CC: _on_joint_command(JointState)
    CC->>VJ: Route to velocity task

    loop Every tick (100Hz)
        TL->>CTB: read_state()
        CTB->>Adapter: read_velocities() + read_odometry()
        Adapter-->>CTB: velocities, odometry
        CTB-->>TL: {joint: JointState}
        TL->>TL: Arbitrate (per-joint, priority)
        TL->>CTB: write_command(velocities, mode)
        CTB->>Adapter: write_velocities([vx, vy, wz])
    end
Loading

Last reviewed commit: d62d44e

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

component: HardwareComponent,
) -> bool:
"""Register a hardware adapter with the coordinator."""
from dimos.hardware.drive_trains.spec import TwistBaseAdapter as TwistBaseAdapterProto
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this be at the top?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, will do

self._current_mode: ControlMode | None = None

@property
def adapter(self) -> TwistBaseAdapter: # type: ignore[override]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix type: ignore

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still an ignore here.

def connect(self) -> bool:
"""Connect to FlowBase controller via Portal RPC."""
try:
import portal # type: ignore[import-not-found]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this? It's not in pyproject.toml.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the RPC protocol that the flowbase hardware uses. I need to send messages over it to be able to communicate with the robot.

This is locally installed on the hardware so probable why I never needed to add it to the toml.

These sort of random libraries and packages will be specific to the hardware we work with, Does it make sense still add them to the toml? The risk being the toml keeps growing as we keep adding more hardware and their dependency. Is there another option?
@paul-nechifor

Copy link
Contributor

@paul-nechifor paul-nechifor Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I get so much GitHub spam that I regularly miss updates from GitHub.

I think you should add it to a group in pyproject.toml otherwise people won't know what portal is and how to install it. Oh, it is in pyproject.toml now. 😅

@mustafab0 mustafab0 force-pushed the feature/mustafa-controlcoordinator-support-for-mobile-base branch 4 times, most recently from beea8ec to 8495219 Compare February 20, 2026 22:01
if hw is None:
logger.warning(f"Hardware '{hardware_id}' not found for gripper command")
return False
if isinstance(hw, ConnectedTwistBase):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally want to automatically check things like this with protocols. So as we scale up to integrate with many robots that don't have an end effector we can do checks like this more systematically

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

The plan is to categories all possible hardware in 4-5 different buckets, for example Whole Body Controllers, Twist Base, Manipulators, End effectors etc.

A more sophisticated system will be introduced to perform these checks as you suggested.

Creating a issue ticket to track this.

if hw.component.hardware_type != HardwareType.BASE:
continue
for joint_name in hw.joint_names:
# Extract suffix (e.g., "base_vx" → "vx")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Youre assuming here that joint names MUST have vy and vy and vz etc. Do you know this for sure?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ControlCoordinator is hardware-agnostic, it only operates on named joints, whether the hardware is a 6-DOF arm, 7-DOF arm, or 29-DOF humanoid.

Mobile bases are fundamentally different: they don't accept joint state commands, they accept Twist (cmd_vel). To keep the coordinator architecture uniform, we model a mobile base as a virtual 3-DOF robot with abstract joints suffixed _vx, _vy, _wz. The coordinator ticks these joints like any other, and ConnectedTwistBase unpacks them back into a Twist before dispatching to the hardware.

Any robot accepting Twist for e.g FlowBase, quadrupeds, the G1 works as long as the joints follow this convention.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense just a bit hacky. Would propose a quick fix in future PR but low priority since we only have one type of base integrated.

@spomichter
Copy link
Contributor

Weird module behavior:

python -m dimos.control.examples.twist_base_keyboard_teleop
/home/stash/dimensional/dimos/.venv/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
pygame 2.6.1 (SDL 2.28.4, Python 3.12.12)
Hello from the pygame community. https://www.pygame.org/contribute.html
◟ Initializing dimos local cluster with 2 workers/home/stash/dimensional/dimos/.venv/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
/home/stash/dimensional/dimos/.venv/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
◜ Initializing dimos local cluster with 2 workerspygame 2.6.1 (SDL 2.28.4, Python 3.12.12)
pygame 2.6.1 (SDL 2.28.4, Python 3.12.12)
Hello from the pygame community. https://www.pygame.org/contribute.html
Hello from the pygame community. https://www.pygame.org/contribute.html
Initialized dimos local cluster with 2 workers, memory limit: auto
2026-02-21T10:47:28.433204Z [info     ] Deploying module.                                            [dimos/core/__init__.py] module=ControlCoordinator
2026-02-21T10:47:28.455967Z [info     ] ControlCoordinator initialized at 100.0Hz                    [dimos/control/coordinator.py]
2026-02-21T10:47:28.460761Z [info     ] Deployed module.                                             [dimos/core/__init__.py] module=ControlCoordinator worker_id=1
2026-02-21T10:47:28.480172Z [info     ] Transport                                                    [dimos/core/blueprints.py] module=ControlCoordinator name=joint_state original_name=joint_state topic=/coordinator/joint_state#sensor_msgs.JointState transport=LCMTransport type=dimos.msgs.sensor_msgs.JointState.JointState
2026-02-21T10:47:28.480737Z [info     ] Transport                                                    [dimos/core/blueprints.py] module=ControlCoordinator name=joint_command original_name=joint_command topic=/joint_command#sensor_msgs.JointState transport=LCMTransport type=dimos.msgs.sensor_msgs.JointState.JointState
2026-02-21T10:47:28.481238Z [info     ] Transport                                                    [dimos/core/blueprints.py] module=ControlCoordinator name=cartesian_command original_name=cartesian_command topic=/cartesian_command#geometry_msgs.PoseStamped transport=LCMTransport type=dimos.msgs.geometry_msgs.PoseStamped.PoseStamped
2026-02-21T10:47:28.481740Z [info     ] Transport                                                    [dimos/core/blueprints.py] module=ControlCoordinator name=twist_command original_name=twist_command topic=/cmd_vel#geometry_msgs.Twist transport=LCMTransport type=dimos.msgs.geometry_msgs.Twist.Twist
2026-02-21T10:47:28.482181Z [info     ] Transport                                                    [dimos/core/blueprints.py] module=ControlCoordinator name=buttons original_name=buttons topic=/buttons#std_msgs.UInt32 transport=LCMTransport type=dimos.teleop.quest.quest_types.Buttons
2026-02-21T10:47:28.485935Z [info     ] Added hardware base with joints: ['base_vx', 'base_vy', 'base_wz'] [dimos/control/coordinator.py]
2026-02-21T10:47:30.429536Z [info     ] JointVelocityTask vel_base initialized for joints: ['base_vx', 'base_vy', 'base_wz'] [dimos/control/tasks/velocity_task.py]
2026-02-21T10:47:30.429852Z [info     ] Added task vel_base                                          [dimos/control/coordinator.py]
2026-02-21T10:47:30.430934Z [info     ] TickLoop started at 100.0Hz                                  [dimos/control/tick_loop.py]
2026-02-21T10:47:30.448155Z [info     ] Subscribed to joint_command for streaming tasks              [dimos/control/coordinator.py]
2026-02-21T10:47:30.462401Z [info     ] Subscribed to twist_command for twist base control           [dimos/control/coordinator.py]
2026-02-21T10:47:30.462722Z [info     ] ControlCoordinator started at 100.0Hz                        [dimos/control/coordinator.py]
/home/stash/dimensional/dimos/.venv/lib/python3.12/site-packages/distributed/node.py
:187: UserWarning: Port 8787 is already in use.
Perhaps you already have a cluster running?
Hosting the HTTP server on port 43257 instead
  warnings.warn(
◟ Initializing dimos local cluster with 2 workers/home/stash/dimensional/dimos/.venv/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
/home/stash/dimensional/dimos/.venv/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
◜ Initializing dimos local cluster with 2 workerspygame 2.6.1 (SDL 2.28.4, Python 3.12.12)
Hello from the pygame community. https://www.pygame.org/contribute.html
pygame 2.6.1 (SDL 2.28.4, Python 3.12.12)
Hello from the pygame community. https://www.pygame.org/contribute.html
Initialized dimos local cluster with 2 workers, memory limit: auto
2026-02-21T10:47:33.676646Z [info     ] Deploying module.                                            [dimos/core/__init__.py] module=KeyboardTeleop
2026-02-21T10:47:33.708969Z [info     ] Deployed module.                                             [dimos/core/__init__.py] module=KeyboardTeleop worker_id=1
2026-02-21T10:47:33.724523Z [info     ] Transport                                                    [dimos/core/blueprints.py] module=KeyboardTeleop name=cmd_vel original_name=cmd_vel topic=/cmd_vel#geometry_msgs.Twist transport=LCMTransport type=dimos.msgs.geometry_msgs.Twist.Twist
Starting mock twist base coordinator + keyboard teleop...
Coordinator tick loop: 100Hz
Keyboard teleop: 50Hz on /cmd_vel

/home/stash/dimensional/dimos/.venv/lib/python3.12/site-packages/distributed/node.py
:187: UserWarning: Port 8787 is already in use.
Perhaps you already have a cluster running?
Hosting the HTTP server on port 46219 instead
  warnings.warn(
◝ Initializing dimos local cluster with 2 workers/home/stash/dimensional/dimos/.venv/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
/home/stash/dimensional/dimos/.venv/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
pygame 2.6.1 (SDL 2.28.4, Python 3.12.12)
Hello from the pygame community. https://www.pygame.org/contribute.html
pygame 2.6.1 (SDL 2.28.4, Python 3.12.12)
Hello from the pygame community. https://www.pygame.org/contribute.html
Initialized dimos local cluster with 2 workers, memory limit: auto
/home/stash/dimensional/dimos/.venv/lib/python3.12/site-packages/distributed/node.py
:187: UserWarning: Port 8787 is already in use.
Perhaps you already have a cluster running?
Hosting the HTTP server on port 43401 instead
  warnings.warn(
◟ Initializing dimos local cluster with 2 workers/home/stash/dimensional/dimos/.venv/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
/home/stash/dimensional/dimos/.venv/lib/python3.12/site-packages/pygame/pkgdata.py:25: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import resource_stream, resource_exists
pygame 2.6.1 (SDL 2.28.4, Python 3.12.12)
Hello from the pygame community. https://www.pygame.org/contribute.html
◜ Initializing dimos local cluster with 2 workerspygame 2.6.1 (SDL 2.28.4, Python 3.12.12)
Hello from the pygame community. https://www.pygame.org/contribute.html
Initialized dimos local cluster with 2 workers, memory limit: auto

@spomichter
Copy link
Contributor

spomichter commented Feb 21, 2026

And then more problematic one of your 'how to test' commands doesn't run. echo cmd vel doesnt exist

(dimos) stash@daneelsbrain:~/dimensional/dimos$ python -m dimos.control.examples.echo_cmd_vel
/home/stash/dimensional/dimos/.venv/bin/python: No module named dimos.control.examples.echo_cmd_vel
(dimos) stash@daneelsbrain:~/dimensional/dimos$ 

@mustafab0 mustafab0 force-pushed the feature/mustafa-controlcoordinator-support-for-mobile-base branch from 8495219 to ddf29ef Compare February 21, 2026 15:40

Usage:
python -m dimos.control.examples.echo_cmd_vel
python -m dimos.control.examples.echo_cmd_vel --topic /my_cmd_vel
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have this:

uv run dimos topic echo /cmd_vel

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. Will deprecate in next pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me update the PR description and deprecate file now.

@mustafab0 mustafab0 force-pushed the feature/mustafa-controlcoordinator-support-for-mobile-base branch from ddf29ef to 148f9fb Compare February 23, 2026 18:27
Comment on lines 718 to 729
if self._joint_command_unsub:
self._joint_command_unsub()
self._joint_command_unsub = None
if self._cartesian_command_unsub:
self._cartesian_command_unsub()
self._cartesian_command_unsub = None
if self._twist_command_unsub:
self._twist_command_unsub()
self._twist_command_unsub = None
if self._buttons_unsub:
self._buttons_unsub()
self._buttons_unsub = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule of three: if you have three duplicate things, it's best to generalize. You can add:

self._disposables = CompositeDisposable()

in __init__ and do self._disposables.dispose() here.

Comment on lines +18 to +19
- ConnectedHardware: Wraps ManipulatorAdapter for joint-controlled arms
- ConnectedTwistBase: Wraps TwistBaseAdapter for velocity-commanded platforms
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this information be specified in the actual names? I.e. should they be renamed to JointControlledHardware and VelocityControlledHardware?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

I will need to make a few more of these interfaces. I am waiting to re-categorize based on that.

But your suggestion might be the most ideal scenario.

@spomichter spomichter merged commit 0d14934 into dev Feb 23, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for mobile base locomotion with ControlCoordinator

3 participants