Skip to content

Feature: Control Coordinator support for mobile base#1277

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

Feature: Control Coordinator support for mobile base#1277
mustafab0 wants to merge 8 commits intodevfrom
feature/mustafa-controlcoordinator-support-for-mobile-base

Conversation

@mustafab0
Copy link
Contributor

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

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

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

Comment on lines +45 to +55
coord = coordinator_mock_twist_base.build()

# Build keyboard teleop with LCM transport on /cmd_vel
teleop = (
keyboard_teleop()
.transports(
{
("cmd_vel", Twist): LCMTransport("/cmd_vel", Twist),
}
)
.build()
Copy link
Contributor

Choose a reason for hiding this comment

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

Why build two blueprints instead of just having one?

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.)

# 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.

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?

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]

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]

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

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.


def read_velocities(self) -> list[float]:
"""Return last commanded velocities (FlowBase doesn't report actual)."""
return self._last_velocities.copy()
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not certain, but at a quick glance, it looks like _last_velocities is written to and read from different threads. Does this need with self._lock?

@mustafab0 mustafab0 force-pushed the feature/mustafa-controlcoordinator-support-for-mobile-base branch from 1e88865 to c595eb7 Compare February 17, 2026 20:18
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

2 participants

Comments