Skip to content

Comments

Feature: Control Coordinator support for mobile base#1277

Open
mustafab0 wants to merge 13 commits intodevfrom
feature/mustafa-controlcoordinator-support-for-mobile-base
Open

Feature: Control Coordinator support for mobile base#1277
mustafab0 wants to merge 13 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: python -m dimos.control.examples.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

if hasattr(module, "register"):
module.register(self)
except ImportError as e:
logger.debug(f"Skipping twist base adapter {name}: {e}")
Copy link
Contributor

Choose a reason for hiding this comment

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

debug is hidden by default. It should be at least a warning, but even if you change it to a warning, why skip over broken adapters?

Copy link
Contributor Author

@mustafab0 mustafab0 Feb 19, 2026

Choose a reason for hiding this comment

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

Without the skip, importing the registry would crash other hardware that needs to be loaded, even ones that don't use that adapter.

For example if you are loading 2 arms + one base. If one arm adapter fails everything fails, which is acceptable since if HW is not functional then things shouldn't run. But then it means you have to fix 1 arm and then check if everything is loaded properly.

So if you had a camera + robot base + arm setup. If you skip over broken adapters you can check all the hardware was brought up and what crashed at the same time. So you can fix multiple issues at once rather than waiting to see if things fail again.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed error to warning.

Comment on lines 67 to 70
try:
coord.loop()
except KeyboardInterrupt:
pass
Copy link
Contributor

@paul-nechifor paul-nechifor Feb 17, 2026

Choose a reason for hiding this comment

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

This seems vibed. :) loop handles KeyboardInterrupt so it would never be received there.

(Also, loop calls stop when KeyboardInterrupt is received, so you don't have to call it too.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

All my examples are vibed. Fixed.

# Mock holonomic twist base (3-DOF: vx, vy, wz)
_base_joints = make_twist_base_joints("base")
coordinator_mock_twist_base = control_coordinator(
tick_rate=100.0,
Copy link
Contributor

Choose a reason for hiding this comment

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

I've looked at the codebase, and every since instance of control_coordinator sets tick_rate to 100.0. But that's already the default, so you don't need to set it. joint_state_frame_id is always "coordinator".

I think blueprints should only specify what is different. Many of these values are the default so they shouldn't be mentioned at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wanted to keep things explicit for new users

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

Comment on lines 362 to 367
adapter=adapter, # type: ignore[arg-type]
component=component,
)
else:
connected = ConnectedHardware(
adapter=adapter, # type: ignore[arg-type]
Copy link
Contributor

Choose a reason for hiding this comment

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

Why # type: ignore[arg-type]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why because mypy :'(

The ignores exist because mypy can't narrow the union ManipulatorAdapter | TwistBaseAdapter through the is_base boolean variable.

Fixed this by using isinstance directly in the branch so mypy can narrow the type. ( will confirm if tests pass)

if not isinstance(adapter, TwistBaseAdapterProto):
raise TypeError("adapter must implement TwistBaseAdapter")

self._adapter: TwistBaseAdapter = adapter # type: ignore[assignment]
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[assignment]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

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

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

@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

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?

@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
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