Feature: Control Coordinator support for mobile base#1277
Feature: Control Coordinator support for mobile base#1277
Conversation
Greptile SummaryExtends the
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
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
Last reviewed commit: d62d44e |
| if hasattr(module, "register"): | ||
| module.register(self) | ||
| except ImportError as e: | ||
| logger.debug(f"Skipping twist base adapter {name}: {e}") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Changed error to warning.
| try: | ||
| coord.loop() | ||
| except KeyboardInterrupt: | ||
| pass |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
All my examples are vibed. Fixed.
dimos/control/blueprints.py
Outdated
| # 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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Wanted to keep things explicit for new users
dimos/control/coordinator.py
Outdated
| component: HardwareComponent, | ||
| ) -> bool: | ||
| """Register a hardware adapter with the coordinator.""" | ||
| from dimos.hardware.drive_trains.spec import TwistBaseAdapter as TwistBaseAdapterProto |
There was a problem hiding this comment.
Can't this be at the top?
dimos/control/coordinator.py
Outdated
| adapter=adapter, # type: ignore[arg-type] | ||
| component=component, | ||
| ) | ||
| else: | ||
| connected = ConnectedHardware( | ||
| adapter=adapter, # type: ignore[arg-type] |
There was a problem hiding this comment.
Why # type: ignore[arg-type]
There was a problem hiding this comment.
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)
dimos/control/hardware_interface.py
Outdated
| if not isinstance(adapter, TwistBaseAdapterProto): | ||
| raise TypeError("adapter must implement TwistBaseAdapter") | ||
|
|
||
| self._adapter: TwistBaseAdapter = adapter # type: ignore[assignment] |
There was a problem hiding this comment.
Please fix # type: ignore[assignment]
| self._current_mode: ControlMode | None = None | ||
|
|
||
| @property | ||
| def adapter(self) -> TwistBaseAdapter: # type: ignore[override] |
There was a problem hiding this comment.
Please fix type: ignore
| def connect(self) -> bool: | ||
| """Connect to FlowBase controller via Portal RPC.""" | ||
| try: | ||
| import portal # type: ignore[import-not-found] |
There was a problem hiding this comment.
What is this? It's not in pyproject.toml.
There was a problem hiding this comment.
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
beea8ec to
8495219
Compare
| if hw is None: | ||
| logger.warning(f"Hardware '{hardware_id}' not found for gripper command") | ||
| return False | ||
| if isinstance(hw, ConnectedTwistBase): |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
Youre assuming here that joint names MUST have vy and vy and vz etc. Do you know this for sure?
|
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 |
|
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$ |
8495219 to
ddf29ef
Compare
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 newtwist_command: In[Twist]port converts Twist → virtual joint velocities, feeding the existing routing pipeline.New
TwistBaseAdapterprotocol (10 methods, SI units) provides a lightweight hardware abstraction.ConnectedTwistBaseinheritsConnectedHardwareto keep the tick loop uniform. IncludesMockTwistBaseAdapterfor testing andFlowBaseAdapterfor real holonomic base hardware via Portal RPC.Breaking Changes
None
How to Test
python -m dimos.control.examples.twist_base_keyboard_teleop/cmd_velin another terminal:python -m dimos.control.examples.echo_cmd_velcloses DIM-547
closes DIM-546