Skip to content

feat: integrate gripper into coordinator tick loop#1371

Merged
ruthwikdasyam merged 21 commits intodevfrom
ruthwik/feat/gripper_control
Feb 28, 2026
Merged

feat: integrate gripper into coordinator tick loop#1371
ruthwikdasyam merged 21 commits intodevfrom
ruthwik/feat/gripper_control

Conversation

@ruthwikdasyam
Copy link
Contributor

Problem

Gripper control bypasses the coordinator tick loop - set_gripper_position and get_gripper_position RPCs call the adapter directly, so gripper commands aren't synchronized with arm joints, or published in JointState. This makes gripper control from VR triggers during teleop impossible.

Closes DIM-598
Closes DIM-537

Solution

Integrate gripper into the tick loop as a first-class hardware component.

  • ConnectedGripper wraps the parent arm's adapter, routing read_state()/write_command() through read_gripper_position()/write_gripper_position() so the tick loop handles it uniformly alongside arm joints.
  • HardwareComponent.parent_hardware_id lets gripper components reference their parent arm - no duplicate adapter connections.
  • Analog trigger packing in Buttons - VR trigger floats packed as 7-bit values in bits 16-29 of the existing UInt32 message.
  • TeleopIKTask gripper support - new gripper_joint, gripper_open_pos, gripper_closed_pos config. on_gripper_trigger() maps analog trigger [0,1] → gripper position

Breaking Changes

None

How to Test

  • Hardware test (requires xArm7 + Quest controllers)
  • Squeeze right trigger -- gripper should close proportionally

dimos run arm-teleop-xarm7

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 26, 2026

Greptile Summary

Integrated gripper control into the coordinator tick loop by treating it as a first-class hardware component alongside arm joints.

Key Changes:

  • Added ConnectedGripper wrapper that routes gripper commands through the parent arm's adapter methods (read_gripper_position()/write_gripper_position())
  • Gripper hardware components reference their parent arm via parent_hardware_id to share the same adapter connection
  • Analog VR trigger values (0.0-1.0) packed as 7-bit integers in the Buttons message (bits 16-29) for proportional gripper control
  • TeleopIKTask now supports gripper control via new config fields (gripper_joint, gripper_open_pos, gripper_closed_pos) that map trigger pressure to gripper position
  • XArm adapter modified to enable gripper only once and use non-blocking commands (wait=False) for tick loop compatibility
  • Upgraded xarm6 blueprints to xarm7 (7-DOF) configuration with gripper support
  • Fixed missing _start_control_loop() call in QuestTeleopModule.start() that prevented control loop from starting

Architecture:
The gripper is registered as a separate HardwareComponent with hardware_type=GRIPPER, allowing it to participate in the same tick loop (read → compute → arbitrate → write) and joint arbitration as arm joints. This ensures gripper commands are synchronized with arm movements and properly published in JointState messages.

Confidence Score: 5/5

  • Safe to merge - well-structured integration with clean abstractions and no breaking changes
  • All changes follow established patterns in the codebase. The gripper integration reuses the existing tick loop infrastructure without introducing new complexity. Bit packing math verified, no overflow risk. The xarm adapter changes are appropriate for non-blocking tick loop operation.
  • No files require special attention

Important Files Changed

Filename Overview
dimos/control/hardware_interface.py Added ConnectedGripper class that routes gripper commands through adapter methods - clean implementation
dimos/control/coordinator.py Added gripper hardware setup and config fields - proper parent lookup and adapter reuse
dimos/control/blueprints.py Renamed xarm6 to xarm7, added gripper component and config - configuration changes align with 7-DOF arm
dimos/control/tasks/teleop_task.py Added gripper support with analog trigger mapping - proper resource claiming and joint command integration
dimos/hardware/manipulators/xarm/adapter.py Changed gripper enable to only run once, changed wait=False for non-blocking commands - appropriate for tick loop
dimos/teleop/quest/quest_types.py Added analog trigger packing into Buttons message (7-bit values in bits 16-29) - math verified, no overflow risk

Sequence Diagram

sequenceDiagram
    participant VR as Quest Controller
    participant QT as QuestTeleopModule
    participant TIK as TeleopIKTask
    participant Coord as ControlCoordinator
    participant CG as ConnectedGripper
    participant Adapter as XArmAdapter
    participant HW as xArm Hardware

    Note over VR,HW: VR Trigger Squeeze (Analog 0.0 → 1.0)
    VR->>QT: Joy message (trigger analog)
    QT->>QT: Pack trigger into Buttons (7-bit, bits 23-29)
    QT->>Coord: Publish Buttons message
    Coord->>TIK: on_buttons(msg)
    TIK->>TIK: Decode trigger analog<br/>Map to gripper position
    
    Note over Coord,HW: Coordinator Tick Loop (100Hz)
    Coord->>CG: read_state()
    CG->>Adapter: read_gripper_position()
    Adapter->>HW: get_gripper_position()
    HW-->>Adapter: position (mm)
    Adapter-->>CG: position (meters)
    CG-->>Coord: JointState
    
    Coord->>TIK: compute(state)
    TIK->>TIK: Solve IK for arm<br/>Append gripper position
    TIK-->>Coord: JointCommandOutput<br/>(arm joints + gripper)
    
    Coord->>Coord: Arbitrate commands<br/>(per-joint priority)
    Coord->>CG: write_command(gripper_pos)
    CG->>Adapter: write_gripper_position(pos)
    Adapter->>HW: set_gripper_position(pos_mm, wait=False)
    
    Coord->>Coord: Publish JointState<br/>(arm + gripper)
Loading

Last reviewed commit: 7837c7f

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.

12 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

@mustafab0 mustafab0 left a comment

Choose a reason for hiding this comment

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

Need to justify why a new hardware interface is necessary

Comment on lines 726 to +729
"coordinator_teleop_dual",
"coordinator_teleop_piper",
"coordinator_teleop_xarm6",
"coordinator_teleop_xarm7",
Copy link
Contributor

Choose a reason for hiding this comment

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

This is okay and not a problem at all. But let's thin out control blueprints in the future to only have 1 example of each use cases and only with mock hardware

For example control blueprints should have coordinator_teleop_mock, coordinator_visualservo-mock, coordinator-dual-mock.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, blueprints are overflowing on the coordinator/manipulators end. We can reduce the list and have smaller definitions.


def __init__(
self,
adapter: ManipulatorAdapter,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need ManipulatorAdapter here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ConnectedGripper needs ManipulatorAdapter because right now grippers does not have a seperate SDK/adapter and we reply on arm's adapter for gripper controls -- which means same adapter for both.
It reuses the parent arm's adapter to read and write gripper poses.

self._dof = dof
self._arm: XArmAPI | None = None
self._control_mode: ControlMode = ControlMode.POSITION
self._gripper_enabled: bool = False
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this required?

Copy link
Contributor Author

@ruthwikdasyam ruthwikdasyam Feb 26, 2026

Choose a reason for hiding this comment

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

I added gripper_enabled to run arm.gripper_enable method to activate gripper. But, i see that from manipulation_module, gripper is able to take commands without any arm.gripper_enable method. - which means gripper should always be active.

I can remove this check, if we get some issues later, we will add it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think okay to keep it here, considering more complicated EE such as pneumatic grippers might want to be enabled disabled to save power,etc.

Comment on lines +104 to +105
Bits 16-22: left trigger analog (7-bit, 0=0.0 … 127=1.0)
Bits 23-29: right trigger analog (7-bit, 0=0.0 … 127=1.0)
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

joints=make_joints("arm", 7),
adapter_type="xarm",
address="192.168.1.210",
address="192.168.2.235",
Copy link
Contributor

Choose a reason for hiding this comment

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

This is personal config, it doesn't belong in a blueprint. One idea would be to have GlobalConfig.arm_ip with a default of None. And you can put ARM_IP=192.168.2.235 in your .env file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True. Currently all manipulation blueprints hardcode the arm IP the same way. Moving these to .env is a good idea - I can do a seperate PR cleaning up across all blueprints

self._initialized = True

def disconnect(self) -> None:
"""No-op: lifecycle is owned by the parent ConnectedHardware."""
Copy link
Contributor

Choose a reason for hiding this comment

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

It's not clear to me why ConnectedHardware.disconnect calls self._adapter.disconnect() to begin with. If the life cycle of self._adapter is managed outside ConnectedHardware then it shouldn't call disconnect to begin with (there no connect method for example).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm that's true. I see connect method called in coordinator.py line-244. perhaps, we can call disconnect from coordinator too.
@mustafab0 should be able to reason this precisely

Copy link
Contributor

@mustafab0 mustafab0 Feb 27, 2026

Choose a reason for hiding this comment

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

seld._adapter is simply a wrapper for the robot sdk. The ConnectedHardware is the actual runtime instance and has a lifecycle.

Every HardwareInterface needs a connect/disconnect which are generic for the ControlCoordinator to call. But the specific order of operations are encoded in the adapter.connect()/disconnect() methods.

@ruthwikdasyam you are right about the ControlCoordinator calling connect, that is an oversight. The coordinator shouldn't directly call connect and should always call the hardware interface to do so.

This likely is the case because when I was initially designing control coordinator a HardwareInterface did not exist at all.

logger.warning(
f"TeleopIKTask {self._name}: gripper_joint is set but hand is not "
f"'left' or 'right' — cannot determine which trigger to use"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

We should rely more on the python type system.

TeleopIKTaskConfig.hand has a type of str with a default of "" (which is invalid).

But if only "left" and "right" are valid, its type should be set to Literal["left", "right"]

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. Was using "" as a catch-all (both left and right), but Literal["left", "right"] is cleaner. Will update both TaskConfig and TeleopIKTaskConfig

mustafab0
mustafab0 previously approved these changes Feb 28, 2026
Copy link
Contributor

@mustafab0 mustafab0 left a comment

Choose a reason for hiding this comment

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

Looks ok. 👍

# Build ordered list for adapter
ordered = self._build_ordered_command()
# Build ordered list for arm joints only
arm_ordered = [self._last_commanded[name] for name in self._arm_joint_names]
Copy link
Contributor

Choose a reason for hiding this comment

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

A bit confused about the semantic change.

Is it because we want to make sure the gripper is added exactly after the specific ConnectedHardware manipulator instance?

Or is it to distinguish between TwistBase Hardware and Manipulator Hardware?

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, its to make sure we write joint commands to this ordered list - which only has arm_joints, and no grippers.

The old _build_ordered_command() iterated all joints including gripper which would break here,
howevr used by TwistBase where it works fine - coz they don't have gripper joints, so joint_names is just the velocity DOFs

paul-nechifor
paul-nechifor previously approved these changes Feb 28, 2026
@github-actions github-actions bot dismissed stale reviews from paul-nechifor and mustafab0 via fb45d3b February 28, 2026 08:18
@ruthwikdasyam ruthwikdasyam merged commit 4ce89a6 into dev Feb 28, 2026
21 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants