Skip to content

R1Pro adapter-free dual grippers on the standard manipulation API - #5019

Open
TomCC7 wants to merge 12 commits into
cc/refactor/r1pro-connectionfrom
cc/feat/r1pro-grippers
Open

TomCC7 wants to merge 12 commits into
cc/refactor/r1pro-connectionfrom
cc/feat/r1pro-grippers

Conversation

@TomCC7

@TomCC7 TomCC7 commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

Overview

Add independent R1Pro grippers through the existing manipulation API. Stacked on #5017.

Problem

Each manipulation group needs its own jaw without changing generic control policy.

Changes

  • Declare jaw positions in metres and reuse the unchanged GripperTask.
  • Bind left/right inputs and manipulation groups separately.
  • Keep vendor conversion, feedback checks and target deduplication in R1ProConnection.
  • Combine jaw resources with the parent's native body-velocity resources.
  • Leave generic control code unchanged from refactor(control): retire shared transport adapters #5017.

Validation

  • Eight focused driver/gripper/assembly checks passed; targeted mypy and Ruff passed.
  • Startup requires actual jaw feedback; recovery alone does not reauthorize an interrupted target.
  • No hardware run. Software stop is not vendor cancellation or proof of torque-off. New CI is unverified.

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

go2 replay realtime (arm64)

Details
Benchmark suite Current: 3920bf8 Previous: 14c62ab Ratio
peak memory 1741.602 MB 1762.355 MB 0.99
peak threads 369 threads 385 threads 0.96
network (transport) 2726.658 MB 2731.013 MB 1.00
disk write 1.863 MB 1.605 MB 1.16
instructions 139.571 G 138.791 G 1.01

This comment was automatically generated by workflow using github-action-benchmark.

@codecov

codecov Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

❌ 18 Tests Failed:

Tests completed Failed Passed Skipped
8045 18 8027 55
View the top 3 failed test(s) by shortest run time
dimos.codebase_checks.test_blueprint_kwargs::test_blueprint_atom_kwargs_match_module_config[piper-teleop]
Stack Traces | 0.004s run time
blueprint_name = 'piper-teleop'
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b462b0b60>

    @pytest.mark.parametrize("blueprint_name", _blueprint_params())
    def test_blueprint_atom_kwargs_match_module_config(
        blueprint_name: str, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Fail when blueprint kwargs cannot be consumed by their target module."""
        # The multi-robot blueprints read ROBOT_IPS at import time.
        monkeypatch.setattr(global_config, "robot_ips", "192.0.2.10,192.0.2.11")
        blueprint = _get_blueprint_or_skip(blueprint_name)
    
        violations: list[str] = []
        for atom in blueprint.blueprints:
            unknown_kwargs = sorted(set(atom.kwargs) - _allowed_kwarg_names(atom.module))
            if unknown_kwargs:
                violations.append(
                    f"{atom.module.__module__}.{atom.module.__name__}: unknown kwargs {unknown_kwargs}"
                )
    
        if violations:
            listing = "\n".join(f"  - {violation}" for violation in violations)
>           raise AssertionError(
                f"Blueprint {blueprint_name!r} passes unknown module kwargs:\n{listing}\n\n"
                "Blueprint kwargs are forwarded into the module constructor. "
                "use fields from the config model or explicitly declared `__init__` keyword names."
            )
E           AssertionError: Blueprint 'piper-teleop' passes unknown module kwargs:
E             - dimos.robot.manipulators.piper.blueprints.teleop.PiperTwistCoordinator: unknown kwargs ['model']
E           
E           Blueprint kwargs are forwarded into the module constructor. use fields from the config model or explicitly declared `__init__` keyword names.

atom       = BlueprintAtom(kwargs={'model': RobotModel(_source_path=PosixPath('.../home/runner/.cache.../robot_assets/sources/9e208...Ref(name='file_info', type=<class 'dimos.msgs.files.File.File'>, direction='out')), module_refs=(), instance_name=None)
blueprint  = Blueprint(blueprints=(BlueprintAtom(kwargs={'instance_name': 'ControlCoordinator', 'model': RobotModel(_source_path=Po...Coordinator', 'arm_joint_state'): 'joint_state'}), requirement_checks=(), configurator_checks=(), hosted_placements=())
blueprint_name = 'piper-teleop'
listing    = "  - dimos.robot.manipulators.piper.blueprints.teleop.PiperTwistCoordinator: unknown kwargs ['model']"
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b462b0b60>
unknown_kwargs = []
violations = ["dimos.robot.manipulators.piper.blueprints.teleop.PiperTwistCoordinator: unknown kwargs ['model']"]

dimos/codebase_checks/test_blueprint_kwargs.py:90: AssertionError
dimos.codebase_checks.test_blueprint_kwargs::test_blueprint_atom_kwargs_match_module_config[xarm6-planner-coordinator]
Stack Traces | 0.004s run time
blueprint_name = 'xarm6-planner-coordinator'
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b4573b350>

    @pytest.mark.parametrize("blueprint_name", _blueprint_params())
    def test_blueprint_atom_kwargs_match_module_config(
        blueprint_name: str, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """Fail when blueprint kwargs cannot be consumed by their target module."""
        # The multi-robot blueprints read ROBOT_IPS at import time.
        monkeypatch.setattr(global_config, "robot_ips", "192.0.2.10,192.0.2.11")
        blueprint = _get_blueprint_or_skip(blueprint_name)
    
        violations: list[str] = []
        for atom in blueprint.blueprints:
            unknown_kwargs = sorted(set(atom.kwargs) - _allowed_kwarg_names(atom.module))
            if unknown_kwargs:
                violations.append(
                    f"{atom.module.__module__}.{atom.module.__name__}: unknown kwargs {unknown_kwargs}"
                )
    
        if violations:
            listing = "\n".join(f"  - {violation}" for violation in violations)
>           raise AssertionError(
                f"Blueprint {blueprint_name!r} passes unknown module kwargs:\n{listing}\n\n"
                "Blueprint kwargs are forwarded into the module constructor. "
                "use fields from the config model or explicitly declared `__init__` keyword names."
            )
E           AssertionError: Blueprint 'xarm6-planner-coordinator' passes unknown module kwargs:
E             - dimos.robot.manipulators.xarm.blueprints.basic.XArmCoordinator: unknown kwargs ['model']
E           
E           Blueprint kwargs are forwarded into the module constructor. use fields from the config model or explicitly declared `__init__` keyword names.

atom       = BlueprintAtom(kwargs={'arm': 'xarm6', 'gripper': 'xarm'}, module=<class 'dimos.robot.manipulators.xarm.connection.XArm...e', type=<class 'dimos.msgs.sensor_msgs.JointState.JointState'>, direction='out')), module_refs=(), instance_name=None)
blueprint  = Blueprint(blueprints=(BlueprintAtom(kwargs={'model': RobotModelConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrp...Coordinator', 'arm_joint_state'): 'joint_state'}), requirement_checks=(), configurator_checks=(), hosted_placements=())
blueprint_name = 'xarm6-planner-coordinator'
listing    = "  - dimos.robot.manipulators.xarm.blueprints.basic.XArmCoordinator: unknown kwargs ['model']"
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b4573b350>
unknown_kwargs = []
violations = ["dimos.robot.manipulators.xarm.blueprints.basic.XArmCoordinator: unknown kwargs ['model']"]

dimos/codebase_checks/test_blueprint_kwargs.py:90: AssertionError
dimos.robot.manipulators.piper.test_connection::test_a_target_past_the_jump_guard_is_refused_and_nothing_reaches_the_sdk
Stack Traces | 0.008s run time
rig = <function rig.<locals>.build at 0xff1bb84f7b00>

    def test_a_target_past_the_jump_guard_is_refused_and_nothing_reaches_the_sdk(
        rig: Callable[..., Rig],
    ) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xff1bb84f7b00>

.../manipulators/piper/test_connection.py:322: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b450febd0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xff1b50e7d3d0>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b450fef00>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b450a5460>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_a_target_past_the_jump_gu0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b450fef00>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b450fef00>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_stopping_sends_the_arm_home_then_puts_it_in_its_emergency_stop
Stack Traces | 0.008s run time
rig = <function rig.<locals>.build at 0xff1bb8575ee0>

    def test_stopping_sends_the_arm_home_then_puts_it_in_its_emergency_stop(
        rig: Callable[..., Rig],
    ) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xff1bb8575ee0>

.../manipulators/piper/test_connection.py:387: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b4508a210>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xff1b4508b3b0>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b4508aa20>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b4508a960>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_stopping_sends_the_arm_ho0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b4508aa20>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b4508aa20>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_the_gripper_is_read_in_metres_in_a_message_of_its_own
Stack Traces | 0.008s run time
rig = <function rig.<locals>.build at 0xff1bb84f76a0>

    def test_the_gripper_is_read_in_metres_in_a_message_of_its_own(rig: Callable[..., Rig]) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xff1bb84f76a0>

.../manipulators/piper/test_connection.py:312: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b44e2e6f0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xff1b44e2eb40>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e2f470>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b44e2efc0>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_the_gripper_is_read_in_me0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e2f470>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e2f470>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_a_failed_start_puts_the_arm_in_its_emergency_stop
Stack Traces | 0.009s run time
rig = <function rig.<locals>.build at 0xff1bb8574d60>
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b450894f0>

    def test_a_failed_start_puts_the_arm_in_its_emergency_stop(
        rig: Callable[..., Rig], monkeypatch: pytest.MonkeyPatch
    ) -> None:
        def stuck(self: PiperConnection, home: list[float], timeout_s: float = 0.0) -> None:
            raise PiperError("the arm did not reach home")
    
        monkeypatch.setattr(PiperConnection, "_go_home", stuck)
        sdk = FakePiper()
        with pytest.raises(PiperError):
>           rig(sdk)

monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b450894f0>
rig        = <function rig.<locals>.build at 0xff1bb8574d60>
sdk        = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b4508a7e0>
stuck      = <function test_a_failed_start_puts_the_arm_in_its_emergency_stop.<locals>.stuck at 0xff1bb8574680>

.../manipulators/piper/test_connection.py:406: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b4508a7e0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xff1b450ce1b0>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b450891c0>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b450894f0>
        sdk        = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b4508a7e0>
        tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_a_failed_start_puts_the_a0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b450891c0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b450891c0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_a_silent_arm_is_not_reported_and_takes_no_joint_target
Stack Traces | 0.009s run time
rig = <function rig.<locals>.build at 0xff1bb8574180>
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b45089070>

    def test_a_silent_arm_is_not_reported_and_takes_no_joint_target(
        rig: Callable[..., Rig], monkeypatch: pytest.MonkeyPatch
    ) -> None:
>       r = rig()

monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b45089070>
rig        = <function rig.<locals>.build at 0xff1bb8574180>

.../manipulators/piper/test_connection.py:366: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b45088fb0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xff1b4508b590>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b45088cb0>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b45089070>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_a_silent_arm_is_not_repor0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b45088cb0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b45088cb0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_an_arm_stopped_outside_dimos_takes_no_command_until_its_errors_are_cleared
Stack Traces | 0.009s run time
rig = <function rig.<locals>.build at 0xff1b3fcc7380>

    def test_an_arm_stopped_outside_dimos_takes_no_command_until_its_errors_are_cleared(
        rig: Callable[..., Rig],
    ) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xff1b3fcc7380>

.../manipulators/piper/test_connection.py:335: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b44e2f110>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xff1b44e2fef0>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e2e030>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b44e2edb0>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_an_arm_stopped_outside_di0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e2e030>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e2e030>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_an_arm_with_a_fault_is_not_sent_home_when_stopping
Stack Traces | 0.009s run time
rig = <function rig.<locals>.build at 0xff1bb85745e0>

    def test_an_arm_with_a_fault_is_not_sent_home_when_stopping(rig: Callable[..., Rig]) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xff1bb85745e0>

.../manipulators/piper/test_connection.py:428: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b44e2aa50>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xff1b44e292e0>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e28860>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b44e2b200>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_an_arm_with_a_fault_is_no0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e28860>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e28860>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_joint_state_names_each_joint_with_its_position_speed_and_torque
Stack Traces | 0.009s run time
rig = <function rig.<locals>.build at 0xff1b3fcc7380>

    def test_joint_state_names_each_joint_with_its_position_speed_and_torque(
        rig: Callable[..., Rig],
    ) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xff1b3fcc7380>

.../manipulators/piper/test_connection.py:278: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b450009e0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xff1b44fc0530>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44fc0bc0>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b50e7dbb0>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_joint_state_names_each_jo0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44fc0bc0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44fc0bc0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_joint_targets_go_out_in_thousandths_of_a_degree
Stack Traces | 0.009s run time
rig = <function rig.<locals>.build at 0xff1b3fc913a0>

    def test_joint_targets_go_out_in_thousandths_of_a_degree(rig: Callable[..., Rig]) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xff1b3fc913a0>

.../manipulators/piper/test_connection.py:269: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b4508dfa0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xff1b450cf800>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b450cf920>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b450cf710>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_joint_targets_go_out_in_t0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b450cf920>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b450cf920>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_the_gripper_goes_out_as_its_gap_with_every_command
Stack Traces | 0.009s run time
rig = <function rig.<locals>.build at 0xff1b3fcc7e20>

    def test_the_gripper_goes_out_as_its_gap_with_every_command(rig: Callable[..., Rig]) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xff1b3fcc7e20>

.../manipulators/piper/test_connection.py:295: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b44e2e9c0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xff1b44e2e990>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e2ef00>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b44e2eed0>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_the_gripper_goes_out_as_i0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e2ef00>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e2ef00>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_start_resets_the_arm_turns_its_motors_on_and_sends_it_home
Stack Traces | 0.01s run time
rig = <function rig.<locals>.build at 0xff1b3fcc76a0>

    def test_start_resets_the_arm_turns_its_motors_on_and_sends_it_home(
        rig: Callable[..., Rig],
    ) -> None:
        sdk = FakePiper(arm_status=COLLISION)
        sdk.positions = [10_000, 0, 0, 0, 0, 0]
>       r = rig(sdk)

rig        = <function rig.<locals>.build at 0xff1b3fcc76a0>
sdk        = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b44e9b7a0>

.../manipulators/piper/test_connection.py:244: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b44e9b7a0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xff1b44e98dd0>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e99be0>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b44e98650>
        sdk        = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b44e9b7a0>
        tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_start_resets_the_arm_turn0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e99be0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44e99be0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_teach_mode_stops_every_command
Stack Traces | 0.01s run time
rig = <function rig.<locals>.build at 0xff1bb84f6d40>

    def test_teach_mode_stops_every_command(rig: Callable[..., Rig]) -> None:
>       r = rig()

rig        = <function rig.<locals>.build at 0xff1bb84f6d40>

.../manipulators/piper/test_connection.py:355: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/test_connection.py:231: in build
    module.start()
        fake       = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b450a6bd0>
        made       = [<dimos.robot.manipulators.piper.test_connection.Rig object at 0xff1b450a5c40>]
        module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b450a6ed0>
        monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b450a4cb0>
        sdk        = None
        tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_teach_mode_stops_every_co0')
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b450a6ed0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b450a6ed0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...can_port='can9', setup_can=True, gripper=True, speed_percent=50, home=(0.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.robot.manipulators.piper.test_connection::test_a_home_outside_the_limits_is_refused_before_the_arm_is_touched
Stack Traces | 0.012s run time
tmp_path = PosixPath('.../pytest-0/popen-gw1/test_a_home_outside_the_limits0')
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b44ea7080>

    def test_a_home_outside_the_limits_is_refused_before_the_arm_is_touched(
        tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        sdk = FakePiper()
        monkeypatch.setattr(connection, "_open_sdk", lambda port: sdk)
        module = PiperConnection(
            can_port=PORT, home=(10.0, 0.0, 0.0, 0.0, 0.0, 0.0), model=piper_like_model(tmp_path)
        )
        Rig(module, sdk)
        try:
            with pytest.raises(ValueError, match="joint1"):
>               module.start()

module     = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44ea68d0>
monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0xff1b44ea7080>
sdk        = <dimos.robot.manipulators.piper.test_connection.FakePiper object at 0xff1b44ea4a10>
tmp_path   = PosixPath('.../pytest-0/popen-gw1/test_a_home_outside_the_limits0')

.../manipulators/piper/test_connection.py:421: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.../manipulators/piper/connection.py:190: in start
    super().start()
        __class__  = <class 'dimos.robot.manipulators.piper.connection.PiperConnection'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44ea68d0>
.../control/connection/connection_module.py:184: in start
    self._limits = limits_of(self.config)
        __class__  = <class 'dimos.control.connection.connection_module.ConnectionModule'>
        self       = <dimos.robot.manipulators.piper.connection.PiperConnection object at 0xff1b44ea68d0>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

config = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...n_port='can9', setup_can=True, gripper=False, speed_percent=50, home=(10.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)

    def limits(config: ConnectionConfig) -> dict[str, Limits]:
        """Every key a robot accepts, with how far each may go, from its config.
    
        Only declared interfaces get keys, in model order. Declare PD_INTERFACES
        for joints driven by position, velocity, effort and gains together. Bounds
        come from the model; commands outside these bounds are rejected.
    
        Raises:
            ValueError: If joints are commanded without a model, no joint of the
                model moves, the model lacks a bound a command needs, or the
                config commands nothing.
        """
        source = config.source
        if not config.joint_commands:
            raise ValueError(f"{source!r} is told nothing: set joint_commands")
        if config.model is None:
            raise ValueError(f"{source!r} commands joints but has no model: set `model`")
        joints = {joint.name: joint for joint in movable_joints(config.model)}
        if not joints:
            raise ValueError(f"{source!r} has no joints to drive: none in its model moves")
>       unknown = config.joint_commands.keys() - joints.keys()
E       AttributeError: 'tuple' object has no attribute 'keys'

config     = PiperConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts={'bu...n_port='can9', setup_can=True, gripper=False, speed_percent=50, home=(10.0, 0.0, 0.0, 0.0, 0.0, 0.0), jump_limit_s=0.1)
joints     = {'gripper': JointDescription(name='gripper', type='prismatic', parent_link='link6', child_link='gripper_link', origin_....0, 0.0), lower=-2.0, upper=2.0, velocity=3.0, acceleration=None, effort=100.0, axis=(1.0, 0.0, 0.0), mimic=None), ...}
source     = 'arm'

.../control/contract/limits.py:98: AttributeError
dimos.control.benchmarking.test_benchmark::test_end_to_end_controller_benchmark_scoring
Stack Traces | 0.159s run time
tmp_path = PosixPath('.../pytest-0/popen-gw2/test_end_to_end_controller_ben0')

    def test_end_to_end_controller_benchmark_scoring(tmp_path):
        """Prove the full decoupled chain in-process against the in-repo FOPDT sim:
        the benchmark publishes a path+speed (here driven directly), the RPP follower
        tracks it against TwistBasePlantSim, the benchmark records the executed odom,
        odom-based completion fires, a run is written, and the offline scorer emits
        metrics."""
        art = TuningConfig.from_json(DEFAULT_ARTIFACT_PATH)
        task = create_task(
            SimpleNamespace(
                name="rpp_follower", joint_names=_JOINTS, priority=10, params={"speed": 0.5}
            ),
            None,
        )
        plant = TwistBasePlantSim(
            TwistBasePlantParams(
                vx=FopdtChannelParams(art.plant.vx.K, art.plant.vx.tau, art.plant.vx.L),
                vy=FopdtChannelParams(art.plant.vy.K, art.plant.vy.tau, art.plant.vy.L),
                wz=FopdtChannelParams(art.plant.wz.K, art.plant.wz.tau, art.plant.wz.L),
            )
        )
        plant.reset(0.0, 0.0, 0.0, 0.1)
    
        # Benchmark anchors the path to the robot's first odom (here the origin).
        ref = shift_path_to_start_at_pose(straight_line(length=2.0), _pose(plant.x, plant.y, plant.yaw))
        task.start_path(ref, _pose(plant.x, plant.y, plant.yaw))
    
        recorder = OdomRecorder()
        monitor = _monitor(ref, dwell_s=0.3)
        done = False
        for k in range(800):
            out = task.compute(_state(plant.x, plant.y, plant.yaw, t=k * 0.1))
            cvx, cvy, cwz = out.velocities if out is not None else (0.0, 0.0, 0.0)
            # Benchmark records what it sees over the transport: cmd_vel + odom.
            recorder.on_cmd_vel(Twist(linear=Vector3(cvx, cvy, 0.0), angular=Vector3(0.0, 0.0, cwz)))
            recorder.on_odom(_pose(plant.x, plant.y, plant.yaw), now=k * 0.1)
            lin, ang = recorder.body_speed()
            if monitor.update(plant.x, plant.y, lin, ang, k * 0.1):
                done = True
                break
            plant.step(cvx, cvy, cwz, 0.1)
>       assert done, "odom-based completion never fired"
E       AssertionError: odom-based completion never fired
E       assert False

ang        = 0.0
art        = TuningConfig(provenance=Provenance(robot_id='go2', surface='concrete', mode='default', date='2026-06-23', git_sha='972...3, L=0.15, r2=0.9999577115547055, source='sweep')]), floor_probe_results=None, valid_for_tuning=True, schema_version=1)
cvx        = 0.0
cvy        = 0.0
cwz        = 0.0
done       = False
k          = 799
lin        = 0.0
monitor    = <dimos.control.benchmarking.benchmark.CompletionMonitor object at 0xfffd7c7ba2a0>
out        = None
plant      = <dimos.control.benchmarking.plant.TwistBasePlantSim object at 0xfffd7c772c00>
recorder   = <dimos.control.benchmarking.benchmark.OdomRecorder object at 0xfffd7c7ba270>
ref        = <dimos.msgs.nav_msgs.Path.Path object at 0xfffd7c7b8c20>
task       = <dimos.control.tasks.rpp_path_follower_task.rpp_path_follower_task.RPPPathFollowerTask object at 0xfffd7c770050>
tmp_path   = PosixPath('.../pytest-0/popen-gw2/test_end_to_end_controller_ben0')

.../control/benchmarking/test_benchmark.py:326: AssertionError
dimos.control.benchmarking.test_benchmark::test_end_to_end_fullpose_benchmark_scoring
Stack Traces | 0.16s run time
tmp_path = PosixPath('.../pytest-0/popen-gw2/test_end_to_end_fullpose_bench0')

    def test_end_to_end_fullpose_benchmark_scoring(tmp_path):
        """Same decoupled chain, full-pose flavor: the holonomic follower tracks a
        translate-while-rotating path against the FOPDT sim, completion fires from
        odom, and the offline scorer reports heading error against the COMMANDED
        yaw (which the tangent-facing followers could not keep small)."""
        from dimos.control.tasks.holonomic_pose_follower_task.holonomic_pose_follower_task import (
            create_task as create_holonomic_task,
        )
    
        art = TuningConfig.from_json(DEFAULT_ARTIFACT_PATH)
        task = create_holonomic_task(
            SimpleNamespace(
                name="holonomic_follower", joint_names=_JOINTS, priority=10, params={"speed": 0.5}
            ),
            None,
        )
        plant = TwistBasePlantSim(
            TwistBasePlantParams(
                vx=FopdtChannelParams(art.plant.vx.K, art.plant.vx.tau, art.plant.vx.L),
                vy=FopdtChannelParams(art.plant.vy.K, art.plant.vy.tau, art.plant.vy.L),
                wz=FopdtChannelParams(art.plant.wz.K, art.plant.wz.tau, art.plant.wz.L),
            )
        )
        plant.reset(0.0, 0.0, 0.0, 0.1)
    
        ref = shift_path_to_start_at_pose(
            straight_rotate(length=3.0), _pose(plant.x, plant.y, plant.yaw)
        )
        task.start_path(ref, _pose(plant.x, plant.y, plant.yaw))
    
        recorder = OdomRecorder()
        monitor = _monitor(ref, dwell_s=0.3)
        done = False
        for k in range(800):
            out = task.compute(_state(plant.x, plant.y, plant.yaw, t=k * 0.1))
            cvx, cvy, cwz = out.velocities if out is not None else (0.0, 0.0, 0.0)
            recorder.on_cmd_vel(Twist(linear=Vector3(cvx, cvy, 0.0), angular=Vector3(0.0, 0.0, cwz)))
            recorder.on_odom(_pose(plant.x, plant.y, plant.yaw), now=k * 0.1)
            lin, ang = recorder.body_speed()
            if monitor.update(plant.x, plant.y, lin, ang, k * 0.1):
                done = True
                break
            plant.step(cvx, cvy, cwz, 0.1)
>       assert done, "odom-based completion never fired"
E       AssertionError: odom-based completion never fired
E       assert False

ang        = 0.0
art        = TuningConfig(provenance=Provenance(robot_id='go2', surface='concrete', mode='default', date='2026-06-23', git_sha='972...3, L=0.15, r2=0.9999577115547055, source='sweep')]), floor_probe_results=None, valid_for_tuning=True, schema_version=1)
create_holonomic_task = <function create_task at 0xfffe1a3ad4e0>
cvx        = 0.0
cvy        = 0.0
cwz        = 0.0
done       = False
k          = 799
lin        = 0.0
monitor    = <dimos.control.benchmarking.benchmark.CompletionMonitor object at 0xfffd7c636390>
out        = JointCommandOutput(joint_names=['go2/vx', 'go2/vy', 'go2/wz'], positions=None, velocities=[0.0, 0.0, 0.0], efforts=None, mode=<ControlMode.VELOCITY: 'velocity'>)
plant      = <dimos.control.benchmarking.plant.TwistBasePlantSim object at 0xfffd7c634680>
recorder   = <dimos.control.benchmarking.benchmark.OdomRecorder object at 0xfffd7fa96cf0>
ref        = <dimos.msgs.nav_msgs.Path.Path object at 0xfffd7c6363f0>
task       = <dimos.control.tasks.holonomic_pose_follower_task.holonomic_pose_follower_task.HolonomicPoseFollowerTask object at 0xfffd7c634530>
tmp_path   = PosixPath('.../pytest-0/popen-gw2/test_end_to_end_fullpose_bench0')

.../control/benchmarking/test_benchmark.py:396: AssertionError
dimos.robot.galaxea.r1pro.blueprints.manipulation.test_r1pro_planar_preview::test_real_control_declares_planar_velocity_joints_without_position_commands
Stack Traces | 8.44s run time
def test_real_control_declares_planar_velocity_joints_without_position_commands() -> None:
        """Measured planar joints are ordinary velocity-only hardware interfaces."""
        coordinator = _coordinator_config(r1pro_control())
    
        # The real robot is driven through R1ProConnection: no hardware adapters.
        assert coordinator.hardware == []
        claimed = {joint for task in coordinator.tasks for joint in task.joint_names}
        assert claimed == {*R1PRO_UPPER_BODY_JOINTS, *R1PRO_BASE_JOINTS}
        from dimos.control.contract.keys import Interface
    
        (declaration,) = coordinator.connections
>       assert all(
            declaration.joint_commands[joint] == (Interface.VELOCITY,)
            for joint in R1PRO_PLANAR_BASE.joint_names
        )

Interface  = <enum 'Interface'>
claimed    = {'r1pro/base_forward', 'r1pro/base_lateral', 'r1pro/base_turn', 'r1pro/left_arm_joint1', 'r1pro/left_arm_joint2', 'r1pro/left_arm_joint3', ...}
coordinator = ControlCoordinatorConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_..._turn': (<Interface.VELOCITY: 'velocity'>,)}, deadman_timeout_s=0.1)], reading_timeout_s=0.2, zero_stiffness_joints=[])
declaration = ConnectionConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts...': (<Interface.VELOCITY: 'velocity'>,), 'r1pro/base_turn': (<Interface.VELOCITY: 'velocity'>,)}, deadman_timeout_s=0.1)

.../blueprints/manipulation/test_r1pro_planar_preview.py:123: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

.0 = <tuple_iterator object at 0xff1b44dc49a0>

    assert all(
>       declaration.joint_commands[joint] == (Interface.VELOCITY,)
        for joint in R1PRO_PLANAR_BASE.joint_names
    )
E   KeyError: 'r1pro/base_x'

.0         = <tuple_iterator object at 0xff1b44dc49a0>
Interface  = <enum 'Interface'>
declaration = ConnectionConfig(rpc_transport=<class 'dimos.protocol.rpc.zenohrpc.ZenohRPC'>, default_rpc_timeout=120.0, rpc_timeouts...': (<Interface.VELOCITY: 'velocity'>,), 'r1pro/base_turn': (<Interface.VELOCITY: 'velocity'>,)}, deadman_timeout_s=0.1)
joint      = 'r1pro/base_x'

.../blueprints/manipulation/test_r1pro_planar_preview.py:124: KeyError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

priority: int = 10
hold_duration: float = 0.0
open_at: OpenAt = "upper"
require_feedback: bool = False

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

should not need these addition. r1pro gripper handling should stay within connection module

@TomCC7
TomCC7 marked this pull request as ready for review November 10, 2026 20:16
@greptile-apps

greptile-apps Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The PR adds independent jaw actuation to the R1Pro manipulation workflow.

Fix the uncleared-fault command path and the test race before merging.

Findings

  1. P1 Uncleared faults stop blocking commands ▶
  2. P2 Jaw test can fail randomly ▶

Summary

Adds independent left and right R1Pro jaws through ordinary position joints and the existing manipulation API.

  • The R1Pro manipulation API now opens each jaw on its own command stream.

Other reviewed concerns were set aside:

  • Initial readiness requires both jaws, explicitly intentional in TomCC7's PR description.
  • Reissuing the same interrupted opening remains blocked, explicitly intentional in TomCC7's PR description; a different opening is required.
  • Generic gripper readback retains its last measurement, explicitly acknowledged by TomCC7 as not being a freshness signal.
  • Software stop does not cancel an accepted vendor jaw target, explicitly acknowledged by TomCC7 and documented in the changed README.
  • Future or zero feedback timestamps still expire by receipt age; indefinite authorization was not established.
  • Finite out-of-range feedback had no established supported-device failure path.
  • ConnectionModule serializes writes and halt calls with _hardware_lock.
  • The inspected model limits, jaw units, group mapping, and task bindings agree with the new names and ranges.
  • The fake's empty-status description conflicts with the callback, but the vendor's empty-report contract was not established.

Reviews (1) · Last reviewed commit: "refactor(r1pro): trim gripper channel st..." · Reviewed by Greptile

return (
self.measured_m is not None
and now - self.feedback_at <= timeout_s
and all(now - at > timeout_s for at in self.faults.values())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Uncleared faults stop blocking commands

If the status stream stalls after reporting a fault while jaw measurements continue, valid() becomes true after gripper_feedback_timeout_s—one second by default. A different opening then reaches the vendor controller despite the unresolved torso, arm, or jaw fault. Keep reported faults blocking until an explicit clear arrives.

Comment on lines +236 to +237
wait_until(lambda: bool(r.jaw_readings()), timeout=5.0)
assert list(r.jaw_readings()[-1].position) == pytest.approx([0.02, 0.04])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Jaw test can fail randomly

feed_grippers() delivers the left and right callbacks separately. The publish thread can run between them and publish only the left jaw. That satisfies this wait, but the next assertion requires both positions.

Wait for a reading containing both expected joint names. This race violates the repository's requirement to avoid flaky tests and use proper synchronization, so it must be fixed before merging.

Rule Used: Do not introduce flaky behavior into the test suite. Both project code and test code must ensure proper cleanup of processes and other resources, and should avoid race conditions caused by timing-based sleeps when synchronization primitives such as l... (source)

This branch was successfully deployed

No deployments
cachix — 07de484f Deployed Nov 10, 2026 by TomCC7 via cachix-build-macos #16900
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.

1 participant